# SPDX-FileCopyrightText: 2020-2025 Nicotine+ Contributors
# SPDX-FileCopyrightText: 2016-2017 Michael Labouebe <gfarmerfr@free.fr>
# SPDX-FileCopyrightText: 2016 Mutnick <muhing@yahoo.com>
# SPDX-FileCopyrightText: 2008-2011 quinox <quinox@users.sf.net>
# SPDX-FileCopyrightText: 2008 gallows <g4ll0ws@gmail.com>
# SPDX-FileCopyrightText: 2006-2009 daelstorm <daelstorm@gmail.com>
# SPDX-FileCopyrightText: 2003-2004 Hyriand <hyriand@thegraveyard.org>
# SPDX-License-Identifier: GPL-3.0-or-later

import os
import re
import sys
import time

from datetime import datetime

from gi.repository import Gdk
from gi.repository import GObject
from gi.repository import Gtk
from gi.repository import Pango

import pynicotine
from pynicotine.config import config
from pynicotine.core import core
from pynicotine.events import events
from pynicotine.gtkgui.application import GTK_API_VERSION
from pynicotine.gtkgui.application import GTK_MINOR_VERSION
from pynicotine.gtkgui.dialogs.pluginsettings import PluginSettings
from pynicotine.gtkgui.popovers.portchecker import PortChecker
from pynicotine.gtkgui.popovers.searchfilterhelp import SearchFilterHelp
from pynicotine.gtkgui.widgets import ui
from pynicotine.gtkgui.widgets.accelerator import Accelerator
from pynicotine.gtkgui.widgets.combobox import ComboBox
from pynicotine.gtkgui.widgets.dialogs import Dialog
from pynicotine.gtkgui.widgets.dialogs import EntryDialog
from pynicotine.gtkgui.widgets.dialogs import MessageDialog
from pynicotine.gtkgui.widgets.dialogs import OptionDialog
from pynicotine.gtkgui.widgets.filechooser import FileChooserButton
from pynicotine.gtkgui.widgets.filechooser import FileChooserSave
from pynicotine.gtkgui.widgets.filechooser import FolderChooser
from pynicotine.gtkgui.widgets.infobar import InfoBar
from pynicotine.gtkgui.widgets.popupmenu import PopupMenu
from pynicotine.gtkgui.widgets.textentry import SpellChecker
from pynicotine.gtkgui.widgets.textview import TextView
from pynicotine.gtkgui.widgets.theme import USER_STATUS_ICON_NAMES
from pynicotine.gtkgui.widgets.theme import add_css_class
from pynicotine.gtkgui.widgets.theme import load_custom_icons
from pynicotine.gtkgui.widgets.theme import set_dark_mode
from pynicotine.gtkgui.widgets.theme import update_custom_css
from pynicotine.gtkgui.widgets.treeview import TreeView
from pynicotine.i18n import LANGUAGES
from pynicotine.logfacility import log
from pynicotine.shares import PermissionLevel
from pynicotine.slskmessages import UserStatus
from pynicotine.slskproto import NetworkInterfaces
from pynicotine.utils import encode_path
from pynicotine.utils import open_folder_path
from pynicotine.utils import open_uri
from pynicotine.utils import unescape


class NetworkPage:

    def __init__(self, application):

        (
            self.auto_away_spinner,
            self.auto_connect_startup_toggle,
            self.auto_reply_message_entry,
            self.check_port_status_button,
            self.check_port_status_label,
            self.container,
            self.current_port_label,
            self.listen_port_spinner,
            self.network_interface_label,
            self.soulseek_server_entry,
            self.upnp_toggle,
            self.username_entry
        ) = self.widgets = ui.load(scope=self, path="settings/network.ui")

        self.application = application

        self.username_entry.set_max_length(core.users.USERNAME_MAX_LENGTH)

        for event_name, callback in (
            ("server-disconnect", self.update_port),
            ("server-login", self.update_port)
        ):
            events.connect(event_name, callback)

        self.port_checker = PortChecker(application.preferences)
        self.port_checker.set_menu_button(self.check_port_status_button)

        if GTK_API_VERSION >= 4:
            inner_button = next(iter(self.check_port_status_button))
            self.check_port_status_label.set_mnemonic_widget(inner_button)

        self.network_interface_combobox = ComboBox(
            container=self.network_interface_label.get_parent(), has_entry=True,
            label=self.network_interface_label
        )

        self.options = {
            "server": {
                "server": None,  # Special case in set_settings
                "login": self.username_entry,
                "portrange": None,  # Special case in set_settings
                "autoaway": self.auto_away_spinner,
                "autoreply": self.auto_reply_message_entry,
                "interface": self.network_interface_combobox,
                "upnp": self.upnp_toggle,
                "auto_connect_startup": self.auto_connect_startup_toggle
            }
        }

    def destroy(self):
        self.network_interface_combobox.destroy()
        self.__dict__.clear()

    def update_port(self, *_args):

        unknown_label = _("Unknown")

        if core.users.public_port:
            self.current_port_label.set_markup(_("<b>%(ip)s</b>, port %(port)s") % {
                "ip": core.users.public_ip_address or unknown_label,
                "port": core.users.public_port or unknown_label
            })
        else:
            self.current_port_label.set_text(unknown_label)

        self.port_checker.port = core.users.public_port

    def set_settings(self):

        # Network interfaces
        self.network_interface_combobox.freeze()
        self.network_interface_combobox.clear()
        self.network_interface_combobox.append("")

        for interface in NetworkInterfaces.get_interface_addresses():
            self.network_interface_combobox.append(interface)

        self.network_interface_combobox.unfreeze()

        self.application.preferences.set_widgets_data(self.options)
        self.upnp_toggle.get_parent().set_visible(not self.application.isolated_mode)

        # Listening port status
        self.update_port()

        # Special options
        server_hostname, server_port = config.sections["server"]["server"]
        self.soulseek_server_entry.set_text(f"{server_hostname}:{server_port}")

        listen_port, _unused_port = config.sections["server"]["portrange"]
        self.listen_port_spinner.set_value(listen_port)

    def get_settings(self):

        try:
            server_address, server_port = self.soulseek_server_entry.get_text().split(":")
            server_addr = (server_address.strip(), int(server_port.strip()))

        except ValueError:
            server_addr = config.defaults["server"]["server"]

        listen_port = self.listen_port_spinner.get_value_as_int()

        return {
            "server": {
                "server": server_addr,
                "login": self.username_entry.get_text(),
                "portrange": (listen_port, listen_port),
                "autoaway": self.auto_away_spinner.get_value_as_int(),
                "autoreply": self.auto_reply_message_entry.get_text(),
                "interface": self.network_interface_combobox.get_text(),
                "upnp": self.upnp_toggle.get_active(),
                "auto_connect_startup": self.auto_connect_startup_toggle.get_active()
            }
        }

    def on_activate_link(self, _label, url):
        open_uri(url)
        return True

    def on_change_password_response(self, dialog, _response_id, user_status):

        password = dialog.get_entry_value()

        if user_status != core.users.login_status:
            MessageDialog(
                parent=self.application.preferences,
                title=_("Password Change Rejected"),
                message=("Since your login status changed, your password has not been changed. Please try again.")
            ).present()
            return

        if not password:
            self.on_change_password()
            return

        if core.users.login_status == UserStatus.OFFLINE:
            config.sections["server"]["passw"] = password
            config.write_configuration()
            return

        core.users.request_change_password(password)

    def on_change_password(self, *_args):

        if core.users.login_status != UserStatus.OFFLINE:
            message = _("Enter a new password for your Soulseek account:")
        else:
            message = (_("You are currently logged out of the Soulseek network. If you want to change "
                         "the password of an existing Soulseek account, you need to be logged into that account.")
                       + "\n\n"
                       + _("Enter password to use when logging in:"))

        EntryDialog(
            parent=self.application.preferences,
            title=_("Change Password"),
            message=message,
            visibility=False,
            action_button_label=_("_Change"),
            callback=self.on_change_password_response,
            callback_data=core.users.login_status
        ).present()

    def on_default_server(self, *_args):
        server_address, server_port = config.defaults["server"]["server"]
        self.soulseek_server_entry.set_text(f"{server_address}:{server_port}")


class DownloadsPage:

    def __init__(self, application):

        (
            self.accept_sent_files_toggle,
            self.alt_speed_spinner,
            self.autoclear_downloads_toggle,
            self.container,
            self.download_double_click_label,
            self.download_folder_default_button,
            self.download_folder_label,
            self.enable_filters_toggle,
            self.enable_username_subfolders_toggle,
            self.file_finished_command_entry,
            self.filter_list_container,
            self.filter_status_label,
            self.folder_finished_command_entry,
            self.incomplete_folder_default_button,
            self.incomplete_folder_label,
            self.received_folder_default_button,
            self.received_folder_label,
            self.sent_files_permission_container,
            self.speed_spinner,
            self.use_alt_speed_limit_radio,
            self.use_speed_limit_radio,
            self.use_unlimited_speed_radio
        ) = self.widgets = ui.load(scope=self, path="settings/downloads.ui")

        self.application = application

        self.sent_files_permission_combobox = ComboBox(
            container=self.sent_files_permission_container,
            items=(
                (_("No one"), 0),
                (_("Buddies"), 2),
                (_("Trusted buddies"), 3)
            )
        )

        items = [
            (_("Nothing"), 0)
        ]
        if not self.application.isolated_mode:
            items += [
                (_("Open File"), 1),
                (_("Open in File Manager"), 2)
            ]
        items += [
            (_("Search"), 3),
            (_("Pause"), 4),
            (_("Remove"), 5),
            (_("Resume"), 6),
            (_("Browse Folder"), 7)
        ]
        self.download_double_click_combobox = ComboBox(
            container=self.download_double_click_label.get_parent(), label=self.download_double_click_label,
            items=items
        )

        self.download_folder_button = FileChooserButton(
            self.download_folder_label.get_parent(), window=application.preferences,
            label=self.download_folder_label, end_button=self.download_folder_default_button, chooser_type="folder",
            show_open_external_button=not self.application.isolated_mode
        )
        self.incomplete_folder_button = FileChooserButton(
            self.incomplete_folder_label.get_parent(), window=application.preferences,
            label=self.incomplete_folder_label, end_button=self.incomplete_folder_default_button, chooser_type="folder",
            show_open_external_button=not self.application.isolated_mode
        )
        self.received_folder_button = FileChooserButton(
            self.received_folder_label.get_parent(), window=application.preferences,
            label=self.received_folder_label, end_button=self.received_folder_default_button, chooser_type="folder",
            show_open_external_button=not self.application.isolated_mode
        )

        self.filter_syntax_description = _("<b>Syntax</b>: Case-insensitive. If enabled, Python regular expressions "
                                           "can be used, otherwise only wildcard * matches "
                                           "are supported.").replace("<b>", "").replace("</b>", "")
        self.filter_list_view = TreeView(
            application.window, parent=self.filter_list_container, multi_select=True,
            activate_row_callback=self.on_edit_filter,
            delete_accelerator_callback=self.on_remove_filter,
            columns={
                "filter": {
                    "column_type": "text",
                    "title": _("Filter"),
                    "width": 150,
                    "expand_column": True,
                    "default_sort_type": "ascending"
                },
                "regex": {
                    "column_type": "toggle",
                    "title": _("Regex"),
                    "width": 0,
                    "toggle_callback": self.on_toggle_regex
                }
            }
        )

        self.filter_popup_menu = PopupMenu(application, self.filter_list_view.widget)
        self.filter_popup_menu.add_items(
            ("#" + _("_Edit…"), self.on_edit_filter),
            ("", None),
            ("#" + _("Remove"), self.on_remove_filter)
        )

        self.options = {
            "transfers": {
                "autoclear_downloads": self.autoclear_downloads_toggle,
                "remotedownloads": self.accept_sent_files_toggle,
                "uploadallowed": self.sent_files_permission_combobox,
                "incompletedir": self.incomplete_folder_button,
                "downloaddir": self.download_folder_button,
                "uploaddir": self.received_folder_button,
                "enablefilters": self.enable_filters_toggle,
                "downloadlimit": self.speed_spinner,
                "downloadlimitalt": self.alt_speed_spinner,
                "usernamesubfolders": self.enable_username_subfolders_toggle,
                "afterfinish": self.file_finished_command_entry,
                "afterfolder": self.folder_finished_command_entry,
                "download_doubleclick": self.download_double_click_combobox
            }
        }

    def destroy(self):

        self.filter_popup_menu.destroy()
        self.download_folder_button.destroy()
        self.incomplete_folder_button.destroy()
        self.received_folder_button.destroy()
        self.filter_list_view.destroy()

        self.__dict__.clear()

    def set_settings(self):

        self.filter_list_view.clear()
        self.application.preferences.set_widgets_data(self.options)

        use_speed_limit = config.sections["transfers"]["use_download_speed_limit"]

        if use_speed_limit == "primary":
            self.use_speed_limit_radio.set_active(True)

        elif use_speed_limit == "alternative":
            self.use_alt_speed_limit_radio.set_active(True)

        else:
            self.use_unlimited_speed_radio.set_active(True)

        self.filter_list_view.freeze()

        for item in config.sections["transfers"]["downloadfilters"]:
            if not isinstance(item, list) or len(item) < 2:
                continue

            dfilter, escaped = item
            enable_regex = not escaped

            self.filter_list_view.add_row([dfilter, enable_regex], select_row=False)

        self.filter_list_view.unfreeze()

    def get_settings(self):

        if self.use_speed_limit_radio.get_active():
            use_speed_limit = "primary"

        elif self.use_alt_speed_limit_radio.get_active():
            use_speed_limit = "alternative"

        else:
            use_speed_limit = "unlimited"

        download_filters = []

        for dfilter, iterator in self.filter_list_view.iterators.items():
            enable_regex = self.filter_list_view.get_row_value(iterator, "regex")
            download_filters.append([dfilter, not enable_regex])

        return {
            "transfers": {
                "autoclear_downloads": self.autoclear_downloads_toggle.get_active(),
                "remotedownloads": self.accept_sent_files_toggle.get_active(),
                "uploadallowed": self.sent_files_permission_combobox.get_selected_id(),
                "incompletedir": self.incomplete_folder_button.get_path(),
                "downloaddir": self.download_folder_button.get_path(),
                "uploaddir": self.received_folder_button.get_path(),
                "downloadfilters": download_filters,
                "enablefilters": self.enable_filters_toggle.get_active(),
                "use_download_speed_limit": use_speed_limit,
                "downloadlimit": self.speed_spinner.get_value_as_int(),
                "downloadlimitalt": self.alt_speed_spinner.get_value_as_int(),
                "usernamesubfolders": self.enable_username_subfolders_toggle.get_active(),
                "afterfinish": self.file_finished_command_entry.get_text().strip(),
                "afterfolder": self.folder_finished_command_entry.get_text().strip(),
                "download_doubleclick": self.download_double_click_combobox.get_selected_id()
            }
        }

    def on_default_download_folder(self, *_args):
        self.download_folder_button.set_path(config.defaults["transfers"]["downloaddir"])

    def on_default_incomplete_folder(self, *_args):
        self.incomplete_folder_button.set_path(config.defaults["transfers"]["incompletedir"])

    def on_default_received_folder(self, *_args):
        self.received_folder_button.set_path(config.defaults["transfers"]["uploaddir"])

    def on_toggle_regex(self, list_view, iterator):

        value = list_view.get_row_value(iterator, "regex")
        list_view.set_row_value(iterator, "regex", not value)

        self.on_verify_filter()

    def on_add_filter_response(self, dialog, _response_id, _data):

        dfilter = dialog.get_entry_value()
        enable_regex = dialog.get_option_value()

        iterator = self.filter_list_view.iterators.get(dfilter)

        if iterator is not None:
            self.filter_list_view.set_row_value(iterator, "regex", enable_regex)
        else:
            self.filter_list_view.add_row([dfilter, enable_regex])

        self.on_verify_filter()

    def on_add_filter(self, *_args):

        EntryDialog(
            parent=self.application.preferences,
            title=_("Add Download Filter"),
            message=self.filter_syntax_description + "\n\n" + _("Enter a new download filter:"),
            action_button_label=_("_Add"),
            callback=self.on_add_filter_response,
            option_value=False,
            option_label=_("Enable regular expressions"),
            droplist=self.filter_list_view.iterators
        ).present()

    def on_edit_filter_response(self, dialog, _response_id, iterator):

        new_dfilter = dialog.get_entry_value()
        enable_regex = dialog.get_option_value()

        dfilter = self.filter_list_view.get_row_value(iterator, "filter")
        orig_iterator = self.filter_list_view.iterators[dfilter]

        self.filter_list_view.remove_row(orig_iterator)
        self.filter_list_view.add_row([new_dfilter, enable_regex])

        self.on_verify_filter()

    def on_edit_filter(self, *_args):

        for iterator in self.filter_list_view.get_selected_rows():
            dfilter = self.filter_list_view.get_row_value(iterator, "filter")
            enable_regex = self.filter_list_view.get_row_value(iterator, "regex")

            EntryDialog(
                parent=self.application.preferences,
                title=_("Edit Download Filter"),
                message=self.filter_syntax_description + "\n\n" + _("Modify the following download filter:"),
                action_button_label=_("_Edit"),
                callback=self.on_edit_filter_response,
                callback_data=iterator,
                default=dfilter,
                option_value=enable_regex,
                option_label=_("Enable regular expressions")
            ).present()
            return

    def on_remove_filter(self, *_args):

        for iterator in reversed(list(self.filter_list_view.get_selected_rows())):
            dfilter = self.filter_list_view.get_row_value(iterator, "filter")
            orig_iterator = self.filter_list_view.iterators[dfilter]

            self.filter_list_view.remove_row(orig_iterator)

        self.on_verify_filter()

    def on_default_filters(self, *_args):

        self.filter_list_view.clear()
        self.filter_list_view.freeze()

        for download_filter, escaped in config.defaults["transfers"]["downloadfilters"]:
            enable_regex = not escaped
            self.filter_list_view.add_row([download_filter, enable_regex], select_row=False)

        self.filter_list_view.unfreeze()
        self.on_verify_filter()

    def on_verify_filter(self, *_args):

        failed = {}
        outfilter = "(\\\\("

        for dfilter, iterator in self.filter_list_view.iterators.items():
            dfilter = self.filter_list_view.get_row_value(iterator, "filter")
            enable_regex = self.filter_list_view.get_row_value(iterator, "regex")

            if not enable_regex:
                dfilter = re.escape(dfilter)
                dfilter = dfilter.replace("\\*", ".*")

            try:
                re.compile("(" + dfilter + ")")
                outfilter += dfilter

                if dfilter != list(self.filter_list_view.iterators)[-1]:
                    outfilter += "|"

            except re.error as error:
                failed[dfilter] = error

        outfilter += ")$)"

        try:
            re.compile(outfilter)

        except re.error as error:
            failed[outfilter] = error

        if failed:
            errors = ""

            for dfilter, error in failed.items():
                errors += "Filter: %(filter)s Error: %(error)s " % {
                    "filter": dfilter,
                    "error": error
                }

            error = _("%(num)d Failed! %(error)s " % {
                "num": len(failed),
                "error": errors}
            )

            self.filter_status_label.set_text(error)
        else:
            self.filter_status_label.set_text(_("Filters Successful"))


class SharesPage:

    PERMISSION_LEVELS = {
        _("Public"): PermissionLevel.PUBLIC,
        _("Buddies"): PermissionLevel.BUDDY,
        _("Trusted buddies"): PermissionLevel.TRUSTED
    }
    FILTER_LEVELS = {
        _("Applies to files"): _("Files"),
        _("Applies to folders"): _("Folders")
    }

    def __init__(self, application):

        (
            self.container,
            self.file_manager_button,
            self.filter_list_container,
            self.filter_list_page,
            self.rescan_daily_toggle,
            self.rescan_hour_container,
            self.rescan_on_startup_toggle,
            self.reveal_buddy_shares_toggle,
            self.reveal_trusted_shares_toggle,
            self.shares_list_container,
            self.shares_list_page,
            self.stack
        ) = self.widgets = ui.load(scope=self, path="settings/shares.ui")

        self.application = application

        self.last_parent_folder = None
        self.shared_folders = []
        self.buddy_shared_folders = []
        self.trusted_shared_folders = []
        self.share_filters = []

        items = []
        for hour in range(24):
            h_time = datetime.now().replace(hour=hour, minute=0, second=0, microsecond=0).strftime("%X")

            for sep in (":", "."):
                if h_time.count(sep) >= 2:
                    # Remove seconds
                    start, end = h_time.rsplit(sep, 1)
                    h_time = start + end[2:]
                    break

            items.append((h_time, hour))

        self.rescan_hour_combobox = ComboBox(container=self.rescan_hour_container, items=items)

        self.shares_list_view = TreeView(
            application.window, parent=self.shares_list_container, multi_select=True,
            activate_row_callback=self.on_edit_shared_folder, select_row_callback=self.on_select_row,
            delete_accelerator_callback=self.on_remove_shared_folder,
            columns={
                "virtual_name": {
                    "column_type": "text",
                    "title": _("Virtual Folder"),
                    "width": 65,
                    "expand_column": True,
                    "default_sort_type": "ascending"
                },
                "folder": {
                    "column_type": "text",
                    "title": _("Folder"),
                    "width": 150,
                    "expand_column": True
                },
                "accessible_to": {
                    "column_type": "text",
                    "title": _("Accessible To"),
                    "width": 0
                }
            }
        )

        self.shares_popup_menu = PopupMenu(application, self.shares_list_view.widget)
        self.shares_popup_menu.add_items(
            ("#" + _("_Edit…"), self.on_edit_shared_folder),
            ("#" + _("Open in File _Manager"), self.on_open_file_manager),
            ("", None),
            ("#" + _("Remove"), self.on_remove_shared_folder)
        )

        self.filter_syntax_description = _("Syntax: Case-insensitive. Virtual paths containing separators \\ are "
                                           "supported. Wildcard * matches are supported.")
        self.filter_list_view = TreeView(
            application.window, parent=self.filter_list_container, multi_select=True,
            activate_row_callback=self.on_edit_filter,
            delete_accelerator_callback=self.on_remove_filter,
            columns={
                "filter": {
                    "column_type": "text",
                    "title": _("Filter"),
                    "width": 150,
                    "expand_column": True,
                    "default_sort_type": "ascending"
                },
                "applies_to": {
                    "column_type": "text",
                    "title": _("Applies To"),
                    "width": 0
                }
            }
        )

        self.filter_popup_menu = PopupMenu(application, self.filter_list_view.widget)
        self.filter_popup_menu.add_items(
            ("#" + _("_Edit…"), self.on_edit_filter),
            ("", None),
            ("#" + _("Remove"), self.on_remove_filter)
        )

        self.popup_menus = (
            self.shares_popup_menu, self.filter_popup_menu
        )

        for widget, name, title in (
            (self.shares_list_page, "shared_folders", _("Shared Folders")),
            (self.filter_list_page, "filters", _("Filters")),
        ):
            self.stack.add_titled(widget, name, title)

        self.options = {
            "transfers": {
                "rescanonstartup": self.rescan_on_startup_toggle,
                "rescan_shares_daily": self.rescan_daily_toggle,
                "rescan_shares_hour": self.rescan_hour_combobox,
                "reveal_buddy_shares": self.reveal_buddy_shares_toggle,
                "reveal_trusted_shares": self.reveal_trusted_shares_toggle
            }
        }

    def destroy(self):

        for menu in self.popup_menus:
            menu.destroy()

        self.shares_list_view.destroy()
        self.filter_list_view.destroy()

        self.__dict__.clear()

    def set_settings(self):

        self.shares_list_view.clear()
        self.filter_list_view.clear()

        self.shares_list_view.freeze()
        self.filter_list_view.freeze()

        self.application.preferences.set_widgets_data(self.options)

        self.shared_folders = config.sections["transfers"]["shared"][:]
        self.buddy_shared_folders = config.sections["transfers"]["buddyshared"][:]
        self.trusted_shared_folders = config.sections["transfers"]["trustedshared"][:]
        self.share_filters = config.sections["transfers"]["share_filters"][:]

        for virtual_name, folder_path, *_unused in self.shared_folders:
            self.shares_list_view.add_row(
                [virtual_name, folder_path, _("Public")], select_row=False)

        for virtual_name, folder_path, *_unused in self.buddy_shared_folders:
            self.shares_list_view.add_row(
                [virtual_name, folder_path, _("Buddies")], select_row=False)

        for virtual_name, folder_path, *_unused in self.trusted_shared_folders:
            self.shares_list_view.add_row(
                [virtual_name, folder_path, _("Trusted")], select_row=False)

        for sfilter in self.share_filters:
            applies_to = _("Folders") if sfilter.endswith("\\") else _("Files")
            self.filter_list_view.add_row([sfilter, applies_to], select_row=False)

        self.shares_list_view.unfreeze()
        self.filter_list_view.unfreeze()

        self.file_manager_button.set_visible(not self.application.isolated_mode)

    def get_settings(self):

        return {
            "transfers": {
                "shared": self.shared_folders[:],
                "buddyshared": self.buddy_shared_folders[:],
                "trustedshared": self.trusted_shared_folders[:],
                "share_filters": self.share_filters[:],
                "rescanonstartup": self.rescan_on_startup_toggle.get_active(),
                "rescan_shares_daily": self.rescan_daily_toggle.get_active(),
                "rescan_shares_hour": self.rescan_hour_combobox.get_selected_id(),
                "reveal_buddy_shares": self.reveal_buddy_shares_toggle.get_active(),
                "reveal_trusted_shares": self.reveal_trusted_shares_toggle.get_active()
            }
        }

    def on_add_shared_folder_selected(self, selected, _data):

        for folder_path in selected:
            virtual_name = core.shares.add_share(
                folder_path, share_groups=(self.shared_folders, self.buddy_shared_folders, self.trusted_shared_folders)
            )

            if not virtual_name:
                continue

            self.last_parent_folder = os.path.dirname(folder_path)
            self.shares_list_view.add_row([virtual_name, folder_path, _("Public")])

    def on_add_shared_folder(self, *_args):

        # By default, show parent folder of last added share as initial folder
        initial_folder = self.last_parent_folder

        # If present, show parent folder of selected share as initial folder
        for iterator in self.shares_list_view.get_selected_rows():
            initial_folder = os.path.dirname(self.shares_list_view.get_row_value(iterator, "folder"))
            break

        if initial_folder and not os.path.exists(encode_path(initial_folder)):
            initial_folder = None

        FolderChooser(
            parent=self.application.preferences,
            callback=self.on_add_shared_folder_selected,
            title=_("Add a Shared Folder"),
            initial_folder=initial_folder,
            select_multiple=True
        ).present()

    def on_edit_shared_folder_response(self, dialog, _response_id, iterator):

        new_virtual_name = dialog.get_entry_value()
        new_accessible_to = dialog.get_second_entry_value()
        new_accessible_to_short = new_accessible_to.replace(_("Trusted buddies"), _("Trusted"))

        virtual_name = self.shares_list_view.get_row_value(iterator, "virtual_name")
        accessible_to = self.shares_list_view.get_row_value(iterator, "accessible_to")

        if new_virtual_name == virtual_name and new_accessible_to_short == accessible_to:
            return

        folder_path = self.shares_list_view.get_row_value(iterator, "folder")
        permission_level = self.PERMISSION_LEVELS.get(new_accessible_to)
        orig_iterator = self.shares_list_view.iterators[virtual_name]

        self.shares_list_view.remove_row(orig_iterator)
        core.shares.remove_share(
            virtual_name, share_groups=(self.shared_folders, self.buddy_shared_folders, self.trusted_shared_folders)
        )
        new_virtual_name = core.shares.add_share(
            folder_path, permission_level=permission_level, virtual_name=new_virtual_name,
            share_groups=(self.shared_folders, self.buddy_shared_folders, self.trusted_shared_folders),
            validate_path=False
        )

        self.shares_list_view.add_row([new_virtual_name, folder_path, new_accessible_to_short])

    def on_edit_shared_folder(self, *_args):

        for iterator in self.shares_list_view.get_selected_rows():
            virtual_name = self.shares_list_view.get_row_value(iterator, "virtual_name")
            folder_path = self.shares_list_view.get_row_value(iterator, "folder")
            default_item = self.shares_list_view.get_row_value(iterator, "accessible_to")

            EntryDialog(
                parent=self.application.preferences,
                title=_("Edit Shared Folder"),
                message=_("Enter new virtual name for '%(dir)s':") % {"dir": folder_path},
                default=virtual_name,
                second_default=default_item.replace(_("Trusted"), _("Trusted buddies")),
                second_droplist=list(self.PERMISSION_LEVELS),
                use_second_entry=True,
                second_entry_editable=False,
                action_button_label=_("_Edit"),
                callback=self.on_edit_shared_folder_response,
                callback_data=iterator
            ).present()
            return

    def on_remove_shared_folder(self, *_args):

        for iterator in reversed(list(self.shares_list_view.get_selected_rows())):
            virtual_name = self.shares_list_view.get_row_value(iterator, "virtual_name")
            orig_iterator = self.shares_list_view.iterators[virtual_name]

            core.shares.remove_share(
                virtual_name, share_groups=(self.shared_folders, self.buddy_shared_folders, self.trusted_shared_folders)
            )
            self.shares_list_view.remove_row(orig_iterator)

    def on_open_file_manager(self, *_args):

        for iterator in self.shares_list_view.get_selected_rows():
            folder_path = self.shares_list_view.get_row_value(iterator, "folder")
            open_folder_path(folder_path, create_folder=True)
            return

    def on_select_row(self, _list_view, iterator):
        self.file_manager_button.set_sensitive(iterator is not None)

    def process_filter(self, sfilter, applies_to):

        sfilter = sfilter.replace("/", "\\").rstrip("\\")

        if applies_to == _("Folders"):
            suffix = "\\*"
            if sfilter.endswith(suffix):
                sfilter = sfilter[:-len(suffix)]
            sfilter += "\\"

        return sfilter

    def on_add_filter_response(self, dialog, _response_id, _data):

        sfilter = dialog.get_entry_value()
        applies_to = self.FILTER_LEVELS[dialog.get_second_entry_value()]
        sfilter = self.process_filter(sfilter, applies_to)
        iterator = self.filter_list_view.iterators.get(sfilter)

        if iterator is not None:
            return

        self.share_filters.append(sfilter)
        self.filter_list_view.add_row([sfilter, applies_to])

    def on_add_filter(self, *_args):

        EntryDialog(
            parent=self.application.preferences,
            title=_("Add Share Filter"),
            message=self.filter_syntax_description + "\n\n" + _("Enter a new share filter:"),
            second_droplist=self.FILTER_LEVELS,
            use_second_entry=True,
            second_entry_editable=False,
            action_button_label=_("_Add"),
            callback=self.on_add_filter_response,
            droplist=self.filter_list_view.iterators
        ).present()

    def on_edit_filter_response(self, dialog, _response_id, iterator):

        new_sfilter = dialog.get_entry_value()
        new_applies_to = self.FILTER_LEVELS[dialog.get_second_entry_value()]
        new_sfilter = self.process_filter(new_sfilter, new_applies_to)

        sfilter = self.filter_list_view.get_row_value(iterator, "filter")
        orig_iterator = self.filter_list_view.iterators[sfilter]

        self.share_filters.remove(sfilter)
        self.filter_list_view.remove_row(orig_iterator)

        self.share_filters.append(new_sfilter)
        self.filter_list_view.add_row([new_sfilter, new_applies_to])

    def on_edit_filter(self, *_args):

        for iterator in self.filter_list_view.get_selected_rows():
            sfilter = self.filter_list_view.get_row_value(iterator, "filter")
            applies_to = self.filter_list_view.get_row_value(iterator, "applies_to")

            EntryDialog(
                parent=self.application.preferences,
                title=_("Edit Share Filter"),
                message=self.filter_syntax_description + "\n\n" + _("Modify the following share filter:"),
                second_default={v: k for k, v in self.FILTER_LEVELS.items()}[applies_to],
                second_droplist=self.FILTER_LEVELS,
                use_second_entry=True,
                second_entry_editable=False,
                action_button_label=_("_Edit"),
                callback=self.on_edit_filter_response,
                callback_data=iterator,
                default=sfilter
            ).present()
            return

    def on_remove_filter(self, *_args):

        for iterator in reversed(list(self.filter_list_view.get_selected_rows())):
            sfilter = self.filter_list_view.get_row_value(iterator, "filter")
            orig_iterator = self.filter_list_view.iterators[sfilter]

            self.share_filters.remove(sfilter)
            self.filter_list_view.remove_row(orig_iterator)

    def on_default_filters(self, *_args):

        self.share_filters.clear()
        self.filter_list_view.clear()
        self.filter_list_view.freeze()

        self.share_filters = config.defaults["transfers"]["share_filters"][:]

        for sfilter in config.defaults["transfers"]["share_filters"]:
            applies_to = _("Folders") if sfilter.endswith("\\") else _("Files")
            self.filter_list_view.add_row([sfilter, applies_to], select_row=False)

        self.filter_list_view.unfreeze()


class UploadsPage:

    def __init__(self, application):

        (
            self.alt_speed_spinner,
            self.autoclear_uploads_toggle,
            self.container,
            self.limit_total_transfers_radio,
            self.max_queued_files_spinner,
            self.max_queued_size_spinner,
            self.no_buddy_limits_toggle,
            self.prioritize_buddies_toggle,
            self.speed_spinner,
            self.upload_bandwidth_spinner,
            self.upload_double_click_label,
            self.upload_queue_type_label,
            self.upload_slots_spinner,
            self.use_alt_speed_limit_radio,
            self.use_speed_limit_radio,
            self.use_unlimited_speed_radio,
            self.use_upload_slots_bandwidth_radio,
            self.use_upload_slots_fixed_radio
        ) = self.widgets = ui.load(scope=self, path="settings/uploads.ui")

        self.application = application

        items = [
            (_("Nothing"), 0)
        ]
        if not self.application.isolated_mode:
            items += [
                (_("Open File"), 1),
                (_("Open in File Manager"), 2)
            ]
        items += [
            (_("Search"), 3),
            (_("Abort"), 4),
            (_("Remove"), 5),
            (_("Retry"), 6),
            (_("Browse Folder"), 7)
        ]
        self.upload_double_click_combobox = ComboBox(
            container=self.upload_double_click_label.get_parent(), label=self.upload_double_click_label,
            items=items
        )

        self.upload_queue_type_combobox = ComboBox(
            container=self.upload_queue_type_label.get_parent(), label=self.upload_queue_type_label,
            items=(
                (_("Round Robin"), 0),
                (_("First In, First Out"), 1)
            )
        )

        self.options = {
            "transfers": {
                "autoclear_uploads": self.autoclear_uploads_toggle,
                "uploadbandwidth": self.upload_bandwidth_spinner,
                "useupslots": self.use_upload_slots_fixed_radio,
                "uploadslots": self.upload_slots_spinner,
                "uploadlimit": self.speed_spinner,
                "uploadlimitalt": self.alt_speed_spinner,
                "fifoqueue": self.upload_queue_type_combobox,
                "limitby": self.limit_total_transfers_radio,
                "queuelimit": self.max_queued_size_spinner,
                "filelimit": self.max_queued_files_spinner,
                "friendsnolimits": self.no_buddy_limits_toggle,
                "preferfriends": self.prioritize_buddies_toggle,
                "upload_doubleclick": self.upload_double_click_combobox
            }
        }

    def destroy(self):

        self.upload_double_click_combobox.destroy()
        self.upload_queue_type_combobox.destroy()

        self.__dict__.clear()

    def set_settings(self):

        self.application.preferences.set_widgets_data(self.options)

        use_speed_limit = config.sections["transfers"]["use_upload_speed_limit"]

        if use_speed_limit == "primary":
            self.use_speed_limit_radio.set_active(True)

        elif use_speed_limit == "alternative":
            self.use_alt_speed_limit_radio.set_active(True)

        else:
            self.use_unlimited_speed_radio.set_active(True)

    def get_settings(self):

        if self.use_speed_limit_radio.get_active():
            use_speed_limit = "primary"

        elif self.use_alt_speed_limit_radio.get_active():
            use_speed_limit = "alternative"

        else:
            use_speed_limit = "unlimited"

        return {
            "transfers": {
                "autoclear_uploads": self.autoclear_uploads_toggle.get_active(),
                "uploadbandwidth": self.upload_bandwidth_spinner.get_value_as_int(),
                "useupslots": self.use_upload_slots_fixed_radio.get_active(),
                "uploadslots": self.upload_slots_spinner.get_value_as_int(),
                "use_upload_speed_limit": use_speed_limit,
                "uploadlimit": self.speed_spinner.get_value_as_int(),
                "uploadlimitalt": self.alt_speed_spinner.get_value_as_int(),
                "fifoqueue": bool(self.upload_queue_type_combobox.get_selected_id()),
                "limitby": self.limit_total_transfers_radio.get_active(),
                "queuelimit": self.max_queued_size_spinner.get_value_as_int(),
                "filelimit": self.max_queued_files_spinner.get_value_as_int(),
                "friendsnolimits": self.no_buddy_limits_toggle.get_active(),
                "preferfriends": self.prioritize_buddies_toggle.get_active(),
                "upload_doubleclick": self.upload_double_click_combobox.get_selected_id()
            }
        }


class UserProfilePage:

    def __init__(self, application):

        (
            self.container,
            self.description_view_container,
            self.reset_picture_button,
            self.select_picture_label
        ) = self.widgets = ui.load(scope=self, path="settings/userinfo.ui")

        self.application = application

        self.description_view = TextView(self.description_view_container, parse_urls=False)
        self.select_picture_button = FileChooserButton(
            self.select_picture_label.get_parent(), window=application.preferences, label=self.select_picture_label,
            end_button=self.reset_picture_button, chooser_type="image", is_flat=True,
            show_open_external_button=not self.application.isolated_mode
        )

        self.options = {
            "userinfo": {
                "descr": self.description_view,
                "pic": self.select_picture_button
            }
        }

    def destroy(self):

        self.description_view.destroy()
        self.select_picture_button.destroy()

        self.__dict__.clear()

    def set_settings(self):
        self.description_view.clear()
        self.application.preferences.set_widgets_data(self.options)

    def get_settings(self):

        return {
            "userinfo": {
                "descr": repr(self.description_view.get_text()),
                "pic": self.select_picture_button.get_path()
            }
        }

    def on_reset_picture(self, *_args):
        self.select_picture_button.clear()


class IgnoredUsersPage:

    def __init__(self, application):

        (
            self.container,
            self.ignored_ips_container,
            self.ignored_users_container
        ) = self.widgets = ui.load(scope=self, path="settings/ignore.ui")

        self.application = application
        self.added_users = set()
        self.added_ips = set()
        self.removed_users = set()
        self.removed_ips = set()

        self.ignored_users = []
        self.ignored_users_list_view = TreeView(
            application.window, parent=self.ignored_users_container, multi_select=True,
            delete_accelerator_callback=self.on_remove_ignored_user,
            columns={
                "username": {
                    "column_type": "text",
                    "title": _("Username"),
                    "default_sort_type": "ascending"
                }
            }
        )

        self.ignored_users_popup_menu = PopupMenu(application, self.ignored_users_list_view.widget)
        self.ignored_users_popup_menu.add_items(
            ("#" + _("Remove"), self.on_remove_ignored_user)
        )

        self.ignored_ips = {}
        self.ignored_ips_list_view = TreeView(
            application.window, parent=self.ignored_ips_container, multi_select=True,
            delete_accelerator_callback=self.on_remove_ignored_ip,
            columns={
                "ip_address": {
                    "column_type": "text",
                    "title": _("IP Address"),
                    "width": 50,
                    "expand_column": True,
                    "tabular": True
                },
                "user": {
                    "column_type": "text",
                    "title": _("User"),
                    "expand_column": True,
                    "default_sort_type": "ascending"
                }
            }
        )

        self.ignored_ips_popup_menu = PopupMenu(application, self.ignored_ips_list_view.widget)
        self.ignored_ips_popup_menu.add_items(
            ("#" + _("Remove"), self.on_remove_ignored_ip)
        )

        self.popup_menus = (
            self.ignored_users_popup_menu, self.ignored_ips_popup_menu
        )

        self.options = {
            "server": {
                "ignorelist": self.ignored_users_list_view,
                "ipignorelist": self.ignored_ips_list_view
            }
        }

    def destroy(self):

        for menu in self.popup_menus:
            menu.destroy()

        self.ignored_users_list_view.destroy()
        self.ignored_ips_list_view.destroy()

        self.__dict__.clear()

    def set_settings(self):

        self.clear_changes()

        self.ignored_users_list_view.clear()
        self.ignored_ips_list_view.clear()
        self.ignored_users.clear()
        self.ignored_ips.clear()

        self.application.preferences.set_widgets_data(self.options)

        self.ignored_users = config.sections["server"]["ignorelist"][:]
        self.ignored_ips = config.sections["server"]["ipignorelist"].copy()

    def get_settings(self):
        return {}

    def clear_changes(self):

        self.added_users.clear()
        self.added_ips.clear()
        self.removed_users.clear()
        self.removed_ips.clear()

    def on_add_ignored_user_response(self, dialog, _response_id, _data):

        users = dialog.get_entry_value().split("\n")
        is_first_item = True

        for user in users:
            user = user.strip()

            if not user or user in self.ignored_users:
                continue

            self.ignored_users.append(user)
            self.ignored_users_list_view.add_row([str(user)], select_row=is_first_item)

            self.added_users.add(user)
            self.removed_users.discard(user)

            is_first_item = False

    def on_add_ignored_user(self, *_args):

        EntryDialog(
            parent=self.application.preferences,
            title=_("Ignore Users"),
            message=_("Enter a list of usernames you want to ignore:"),
            action_button_label=_("_Add"),
            multiline=True,
            callback=self.on_add_ignored_user_response
        ).present()

    def on_remove_ignored_user(self, *_args):

        for iterator in reversed(list(self.ignored_users_list_view.get_selected_rows())):
            user = self.ignored_users_list_view.get_row_value(iterator, "username")
            orig_iterator = self.ignored_users_list_view.iterators[user]

            self.ignored_users_list_view.remove_row(orig_iterator)
            self.ignored_users.remove(user)

            if user not in self.added_users:
                self.removed_users.add(user)

            self.added_users.discard(user)

    def on_add_ignored_ip_response(self, dialog, _response_id, _data):

        ip_addresses = dialog.get_entry_value().split("\n")
        is_first_item = True

        for ip_address in ip_addresses:
            ip_address = ip_address.strip()

            if not core.network_filter.is_ip_address(ip_address):
                continue

            if ip_address in self.ignored_ips:
                continue

            user = core.network_filter.get_online_username(ip_address) or ""
            user_ip_pair = (user, ip_address)

            self.ignored_ips[ip_address] = user
            self.ignored_ips_list_view.add_row([ip_address, user], select_row=is_first_item)

            self.added_ips.add(user_ip_pair)
            self.removed_ips.discard(user_ip_pair)

            is_first_item = False

    def on_add_ignored_ip(self, *_args):

        EntryDialog(
            parent=self.application.preferences,
            title=_("Ignore IP Addresses"),
            message=_("Enter a list of IP addresses you want to ignore:") + " " + _("* is a wildcard"),
            action_button_label=_("_Add"),
            multiline=True,
            callback=self.on_add_ignored_ip_response
        ).present()

    def on_remove_ignored_ip(self, *_args):

        for iterator in reversed(list(self.ignored_ips_list_view.get_selected_rows())):
            ip_address = self.ignored_ips_list_view.get_row_value(iterator, "ip_address")
            user = self.ignored_ips_list_view.get_row_value(iterator, "user")
            user_ip_pair = (user, ip_address)
            orig_iterator = self.ignored_ips_list_view.iterators[ip_address]

            self.ignored_ips_list_view.remove_row(orig_iterator)
            del self.ignored_ips[ip_address]

            if user_ip_pair not in self.added_ips:
                self.removed_ips.add(user_ip_pair)

            self.added_ips.discard(user_ip_pair)


class BannedUsersPage:

    def __init__(self, application):

        (
            self.ban_message_entry,
            self.ban_message_toggle,
            self.banned_ips_container,
            self.banned_users_container,
            self.container,
            self.geo_block_country_entry,
            self.geo_block_message_entry,
            self.geo_block_message_toggle,
            self.geo_block_toggle
        ) = self.widgets = ui.load(scope=self, path="settings/ban.ui")

        self.application = application
        self.added_users = set()
        self.added_ips = set()
        self.removed_users = set()
        self.removed_ips = set()

        self.banned_users = []
        self.banned_users_list_view = TreeView(
            application.window, parent=self.banned_users_container, multi_select=True,
            delete_accelerator_callback=self.on_remove_banned_user,
            columns={
                "username": {
                    "column_type": "text",
                    "title": _("Username"),
                    "default_sort_type": "ascending"
                }
            }
        )

        self.banned_users_popup_menu = PopupMenu(application, self.banned_users_list_view.widget)
        self.banned_users_popup_menu.add_items(
            ("#" + _("Remove"), self.on_remove_banned_user)
        )

        self.banned_ips = {}
        self.banned_ips_list_view = TreeView(
            application.window, parent=self.banned_ips_container, multi_select=True,
            delete_accelerator_callback=self.on_remove_banned_ip,
            columns={
                "ip_address": {
                    "column_type": "text",
                    "title": _("IP Address"),
                    "width": 50,
                    "expand_column": True,
                    "tabular": True
                },
                "user": {
                    "column_type": "text",
                    "title": _("User"),
                    "expand_column": True,
                    "default_sort_type": "ascending"
                }
            }
        )

        self.banned_ips_popup_menu = PopupMenu(application, self.banned_ips_list_view.widget)
        self.banned_ips_popup_menu.add_items(
            ("#" + _("Remove"), self.on_remove_banned_ip)
        )

        self.popup_menus = (
            self.banned_users_popup_menu, self.banned_ips_popup_menu
        )

        self.options = {
            "server": {
                "banlist": self.banned_users_list_view,
                "ipblocklist": self.banned_ips_list_view
            },
            "transfers": {
                "usecustomban": self.ban_message_toggle,
                "customban": self.ban_message_entry,
                "geoblock": self.geo_block_toggle,
                "geoblockcc": None,
                "usecustomgeoblock": self.geo_block_message_toggle,
                "customgeoblock": self.geo_block_message_entry
            }
        }

    def destroy(self):

        for menu in self.popup_menus:
            menu.destroy()

        self.banned_users_list_view.destroy()
        self.banned_ips_list_view.destroy()

        self.__dict__.clear()

    def set_settings(self):

        self.clear_changes()

        self.banned_users_list_view.clear()
        self.banned_ips_list_view.clear()
        self.banned_users.clear()
        self.banned_ips.clear()

        self.application.preferences.set_widgets_data(self.options)

        self.banned_users = config.sections["server"]["banlist"][:]
        self.banned_ips = config.sections["server"]["ipblocklist"].copy()
        self.geo_block_country_entry.set_text(config.sections["transfers"]["geoblockcc"][0])

    def get_settings(self):

        return {
            "transfers": {
                "usecustomban": self.ban_message_toggle.get_active(),
                "customban": self.ban_message_entry.get_text(),
                "geoblock": self.geo_block_toggle.get_active(),
                "geoblockcc": [self.geo_block_country_entry.get_text().upper()],
                "usecustomgeoblock": self.geo_block_message_toggle.get_active(),
                "customgeoblock": self.geo_block_message_entry.get_text()
            }
        }

    def clear_changes(self):

        self.added_users.clear()
        self.added_ips.clear()
        self.removed_users.clear()
        self.removed_ips.clear()

    def on_add_banned_user_response(self, dialog, _response_id, _data):

        users = dialog.get_entry_value().split("\n")
        is_first_item = True

        for user in users:
            user = user.strip()

            if not user or user in self.banned_users:
                continue

            self.banned_users.append(user)
            self.banned_users_list_view.add_row([user], select_row=is_first_item)

            self.added_users.add(user)
            self.removed_users.discard(user)

            is_first_item = False

    def on_add_banned_user(self, *_args):

        EntryDialog(
            parent=self.application.preferences,
            title=_("Ban Users"),
            message=_("Enter a list of usernames you want to ban:"),
            action_button_label=_("_Add"),
            multiline=True,
            callback=self.on_add_banned_user_response
        ).present()

    def on_remove_banned_user(self, *_args):

        for iterator in reversed(list(self.banned_users_list_view.get_selected_rows())):
            user = self.banned_users_list_view.get_row_value(iterator, "username")
            orig_iterator = self.banned_users_list_view.iterators[user]

            self.banned_users_list_view.remove_row(orig_iterator)
            self.banned_users.remove(user)

            if user not in self.added_users:
                self.removed_users.add(user)

            self.added_users.discard(user)

    def on_add_banned_ip_response(self, dialog, _response_id, _data):

        ip_addresses = dialog.get_entry_value().split("\n")
        is_first_item = True

        for ip_address in ip_addresses:
            ip_address = ip_address.strip()

            if not core.network_filter.is_ip_address(ip_address):
                continue

            if ip_address in self.banned_ips:
                continue

            user = core.network_filter.get_online_username(ip_address) or ""
            user_ip_pair = (user, ip_address)

            self.banned_ips[ip_address] = user
            self.banned_ips_list_view.add_row([ip_address, user], select_row=is_first_item)

            self.added_ips.add(user_ip_pair)
            self.removed_ips.discard(user_ip_pair)

            is_first_item = False

    def on_add_banned_ip(self, *_args):

        EntryDialog(
            parent=self.application.preferences,
            title=_("Ban IP Addresses"),
            message=_("Enter a list of IP addresses you want to ban:") + " " + _("* is a wildcard"),
            action_button_label=_("_Add"),
            multiline=True,
            callback=self.on_add_banned_ip_response
        ).present()

    def on_remove_banned_ip(self, *_args):

        for iterator in reversed(list(self.banned_ips_list_view.get_selected_rows())):
            ip_address = self.banned_ips_list_view.get_row_value(iterator, "ip_address")
            user = self.banned_ips_list_view.get_row_value(iterator, "user")
            user_ip_pair = (user, ip_address)
            orig_iterator = self.banned_ips_list_view.iterators[ip_address]

            self.banned_ips_list_view.remove_row(orig_iterator)
            del self.banned_ips[ip_address]

            if user_ip_pair not in self.added_ips:
                self.removed_ips.add(user_ip_pair)

            self.added_ips.discard(user_ip_pair)


class ChatsPage:

    def __init__(self, application):

        (
            self.auto_replace_words_toggle,
            self.censor_list_container,
            self.censor_list_page,
            self.censor_text_patterns_toggle,
            self.complete_buddy_names_toggle,
            self.complete_commands_toggle,
            self.complete_room_names_toggle,
            self.complete_room_usernames_toggle,
            self.container,
            self.enable_completion_dropdown_toggle,
            self.enable_ctcp_toggle,
            self.enable_spell_checker_toggle,
            self.enable_tab_completion_toggle,
            self.format_codes_label,
            self.min_chars_dropdown_spinner,
            self.private_room_toggle,
            self.recent_private_messages_spinner,
            self.recent_room_messages_spinner,
            self.reopen_private_chats_toggle,
            self.replacement_list_container,
            self.replacement_list_page,
            self.stack,
            self.timestamp_private_chat_entry,
            self.timestamp_room_entry,
        ) = self.widgets = ui.load(scope=self, path="settings/chats.ui")

        self.application = application

        format_codes_url = "https://docs.python.org/3/library/datetime.html#format-codes"
        format_codes_label = _("Format codes")

        self.format_codes_label.set_markup(
            f"<a href='{format_codes_url}' title='{format_codes_url}'>{format_codes_label}</a>")
        self.format_codes_label.connect("activate-link", self.on_activate_link)

        self.censored_patterns = []
        self.censor_list_view = TreeView(
            application.window, parent=self.censor_list_container, multi_select=True,
            activate_row_callback=self.on_edit_censored,
            delete_accelerator_callback=self.on_remove_censored,
            columns={
                "pattern": {
                    "column_type": "text",
                    "title": _("Pattern"),
                    "default_sort_type": "ascending"
                }
            }
        )

        self.censor_popup_menu = PopupMenu(application, self.censor_list_view.widget)
        self.censor_popup_menu.add_items(
            ("#" + _("_Edit…"), self.on_edit_censored),
            ("", None),
            ("#" + _("Remove"), self.on_remove_censored)
        )

        self.replacements = {}
        self.replacement_list_view = TreeView(
            application.window, parent=self.replacement_list_container, multi_select=True,
            activate_row_callback=self.on_edit_replacement,
            delete_accelerator_callback=self.on_remove_replacement,
            columns={
                "pattern": {
                    "column_type": "text",
                    "title": _("Pattern"),
                    "width": 100,
                    "expand_column": True,
                    "default_sort_type": "ascending"
                },
                "replacement": {
                    "column_type": "text",
                    "title": _("Replacement"),
                    "expand_column": True
                }
            }
        )

        self.replacement_popup_menu = PopupMenu(application, self.replacement_list_view.widget)
        self.replacement_popup_menu.add_items(
            ("#" + _("_Edit…"), self.on_edit_replacement),
            ("", None),
            ("#" + _("Remove"), self.on_remove_replacement)
        )

        self.popup_menus = (
            self.censor_popup_menu, self.replacement_popup_menu
        )

        for widget, name, title in (
            (self.replacement_list_page, "auto_replace", _("Auto-Replace")),
            (self.censor_list_page, "censor", _("Censor"))
        ):
            self.stack.add_titled(widget, name, title)

        self.options = {
            "server": {
                "ctcpmsgs": None,  # Special case in set_settings
                "private_chatrooms": self.private_room_toggle
            },
            "logging": {
                "readroomlines": self.recent_room_messages_spinner,
                "readprivatelines": self.recent_private_messages_spinner,
                "rooms_timestamp": self.timestamp_room_entry,
                "private_timestamp": self.timestamp_private_chat_entry
            },
            "privatechat": {
                "store": self.reopen_private_chats_toggle
            },
            "words": {
                "tab": self.enable_tab_completion_toggle,
                "dropdown": self.enable_completion_dropdown_toggle,
                "characters": self.min_chars_dropdown_spinner,
                "roomnames": self.complete_room_names_toggle,
                "buddies": self.complete_buddy_names_toggle,
                "roomusers": self.complete_room_usernames_toggle,
                "commands": self.complete_commands_toggle,
                "censored": self.censor_list_view,
                "censorwords": self.censor_text_patterns_toggle,
                "autoreplaced": self.replacement_list_view,
                "replacewords": self.auto_replace_words_toggle
            },
            "ui": {
                "spellcheck": self.enable_spell_checker_toggle
            }
        }

    def destroy(self):

        for menu in self.popup_menus:
            menu.destroy()

        self.censor_list_view.destroy()
        self.replacement_list_view.destroy()

        self.__dict__.clear()

    def set_settings(self):

        self.censor_list_view.clear()
        self.replacement_list_view.clear()
        self.censored_patterns.clear()
        self.replacements.clear()

        self.application.preferences.set_widgets_data(self.options)

        self.enable_spell_checker_toggle.get_parent().set_visible(SpellChecker.is_available())
        self.enable_ctcp_toggle.set_active(not config.sections["server"]["ctcpmsgs"])
        self.format_codes_label.set_visible(not self.application.isolated_mode)

        self.censored_patterns = config.sections["words"]["censored"][:]
        self.replacements = config.sections["words"]["autoreplaced"].copy()

    def get_settings(self):

        return {
            "server": {
                "ctcpmsgs": not self.enable_ctcp_toggle.get_active(),
                "private_chatrooms": self.private_room_toggle.get_active()
            },
            "logging": {
                "readroomlines": self.recent_room_messages_spinner.get_value_as_int(),
                "readprivatelines": self.recent_private_messages_spinner.get_value_as_int(),
                "private_timestamp": self.timestamp_private_chat_entry.get_text(),
                "rooms_timestamp": self.timestamp_room_entry.get_text()
            },
            "privatechat": {
                "store": self.reopen_private_chats_toggle.get_active()
            },
            "words": {
                "tab": self.enable_tab_completion_toggle.get_active(),
                "dropdown": self.enable_completion_dropdown_toggle.get_active(),
                "characters": self.min_chars_dropdown_spinner.get_value_as_int(),
                "roomnames": self.complete_room_names_toggle.get_active(),
                "buddies": self.complete_buddy_names_toggle.get_active(),
                "roomusers": self.complete_room_usernames_toggle.get_active(),
                "commands": self.complete_commands_toggle.get_active(),
                "censored": self.censored_patterns[:],
                "censorwords": self.censor_text_patterns_toggle.get_active(),
                "autoreplaced": self.replacements.copy(),
                "replacewords": self.auto_replace_words_toggle.get_active()
            },
            "ui": {
                "spellcheck": self.enable_spell_checker_toggle.get_active()
            }
        }

    def on_activate_link(self, _label, url):
        open_uri(url)
        return True

    def on_default_timestamp_room(self, *_args):
        self.timestamp_room_entry.set_text(config.defaults["logging"]["rooms_timestamp"])

    def on_default_timestamp_private_chat(self, *_args):
        self.timestamp_private_chat_entry.set_text(config.defaults["logging"]["private_timestamp"])

    def on_add_censored_response(self, dialog, _response_id, _data):

        patterns = dialog.get_entry_value().split("\n")
        is_first_item = True

        for pattern in patterns:
            if not pattern or pattern in self.censored_patterns:
                continue

            self.censored_patterns.append(pattern)
            self.censor_list_view.add_row([pattern], select_row=is_first_item)

            is_first_item = False

    def on_add_censored(self, *_args):

        EntryDialog(
            parent=self.application.preferences,
            title=_("Censor Patterns"),
            message=_("Enter a list of patterns you want to censor. Add spaces around the pattern if you don't "
                      "want to match strings inside words (may fail at the beginning and end of lines)."),
            action_button_label=_("_Add"),
            multiline=True,
            callback=self.on_add_censored_response
        ).present()

    def on_edit_censored_response(self, dialog, _response_id, iterator):

        pattern = dialog.get_entry_value()

        if not pattern:
            return

        old_pattern = self.censor_list_view.get_row_value(iterator, "pattern")
        orig_iterator = self.censor_list_view.iterators[old_pattern]

        self.censor_list_view.remove_row(orig_iterator)
        self.censored_patterns.remove(old_pattern)

        self.censor_list_view.add_row([pattern])
        self.censored_patterns.append(pattern)

    def on_edit_censored(self, *_args):

        for iterator in self.censor_list_view.get_selected_rows():
            pattern = self.censor_list_view.get_row_value(iterator, "pattern")

            EntryDialog(
                parent=self.application.preferences,
                title=_("Edit Censored Pattern"),
                message=_("Enter a pattern you want to censor. Add spaces around the pattern if you don't "
                          "want to match strings inside words (may fail at the beginning and end of lines)."),
                action_button_label=_("_Edit"),
                callback=self.on_edit_censored_response,
                callback_data=iterator,
                default=pattern
            ).present()
            return

    def on_remove_censored(self, *_args):

        for iterator in reversed(list(self.censor_list_view.get_selected_rows())):
            censor = self.censor_list_view.get_row_value(iterator, "pattern")
            orig_iterator = self.censor_list_view.iterators[censor]

            self.censor_list_view.remove_row(orig_iterator)
            self.censored_patterns.remove(censor)

    def on_add_replacement_response(self, dialog, _response_id, _data):

        pattern = dialog.get_entry_value()
        replacement = dialog.get_second_entry_value()

        if not pattern or not replacement:
            return

        self.replacements[pattern] = replacement
        self.replacement_list_view.add_row([pattern, replacement])

    def on_add_replacement(self, *_args):

        EntryDialog(
            parent=self.application.preferences,
            title=_("Add Replacement"),
            message=_("Enter a text pattern and what to replace it with:"),
            action_button_label=_("_Add"),
            callback=self.on_add_replacement_response,
            use_second_entry=True
        ).present()

    def on_edit_replacement_response(self, dialog, _response_id, iterator):

        pattern = dialog.get_entry_value()
        replacement = dialog.get_second_entry_value()

        if not pattern or not replacement:
            return

        old_pattern = self.replacement_list_view.get_row_value(iterator, "pattern")
        orig_iterator = self.replacement_list_view.iterators[old_pattern]

        self.replacement_list_view.remove_row(orig_iterator)
        del self.replacements[old_pattern]

        self.replacements[pattern] = replacement
        self.replacement_list_view.add_row([pattern, replacement])

    def on_edit_replacement(self, *_args):

        for iterator in self.replacement_list_view.get_selected_rows():
            pattern = self.replacement_list_view.get_row_value(iterator, "pattern")
            replacement = self.replacement_list_view.get_row_value(iterator, "replacement")

            EntryDialog(
                parent=self.application.preferences,
                title=_("Edit Replacement"),
                message=_("Enter a text pattern and what to replace it with:"),
                action_button_label=_("_Edit"),
                callback=self.on_edit_replacement_response,
                callback_data=iterator,
                use_second_entry=True,
                default=pattern,
                second_default=replacement
            ).present()
            return

    def on_remove_replacement(self, *_args):

        for iterator in reversed(list(self.replacement_list_view.get_selected_rows())):
            replacement = self.replacement_list_view.get_row_value(iterator, "pattern")
            orig_iterator = self.replacement_list_view.iterators[replacement]

            self.replacement_list_view.remove_row(orig_iterator)
            del self.replacements[replacement]


class UserInterfacePage:

    def __init__(self, application):

        (
            self.buddy_list_position_label,
            self.chat_colored_usernames_toggle,
            self.chat_username_appearance_label,
            self.close_action_label,
            self.color_chat_action_button,
            self.color_chat_action_entry,
            self.color_chat_command_button,
            self.color_chat_command_entry,
            self.color_chat_highlighted_button,
            self.color_chat_highlighted_entry,
            self.color_chat_local_button,
            self.color_chat_local_entry,
            self.color_chat_remote_button,
            self.color_chat_remote_entry,
            self.color_input_background_button,
            self.color_input_background_entry,
            self.color_input_text_button,
            self.color_input_text_entry,
            self.color_list_text_button,
            self.color_list_text_entry,
            self.color_status_away_button,
            self.color_status_away_entry,
            self.color_status_offline_button,
            self.color_status_offline_entry,
            self.color_status_online_button,
            self.color_status_online_entry,
            self.color_tab_button,
            self.color_tab_changed_button,
            self.color_tab_changed_entry,
            self.color_tab_entry,
            self.color_tab_highlighted_button,
            self.color_tab_highlighted_entry,
            self.color_url_button,
            self.color_url_entry,
            self.container,
            self.dark_mode_toggle,
            self.exact_file_sizes_toggle,
            self.font_browse_button,
            self.font_browse_clear_button,
            self.font_chat_button,
            self.font_chat_clear_button,
            self.font_global_button,
            self.font_global_clear_button,
            self.font_list_button,
            self.font_list_clear_button,
            self.font_search_button,
            self.font_search_clear_button,
            self.font_text_view_button,
            self.font_text_view_clear_button,
            self.font_transfers_button,
            self.font_transfers_clear_button,
            self.header_bar_toggle,
            self.icon_theme_clear_button,
            self.icon_theme_label,
            self.icon_view,
            self.language_label,
            self.minimize_tray_startup_toggle,
            self.notification_chatroom_mention_toggle,
            self.notification_chatroom_toggle,
            self.notification_download_file_toggle,
            self.notification_download_folder_toggle,
            self.notification_private_message_toggle,
            self.notification_queued_upload_toggle,
            self.notification_sounds_toggle,
            self.notification_window_title_toggle,
            self.notification_wish_toggle,
            self.reverse_file_paths_toggle,
            self.tab_close_buttons_toggle,
            self.tab_position_browse_label,
            self.tab_position_chatrooms_label,
            self.tab_position_main_label,
            self.tab_position_private_chat_label,
            self.tab_position_search_label,
            self.tab_position_userinfo_label,
            self.tab_restore_startup_toggle,
            self.tab_visible_browse_toggle,
            self.tab_visible_chatrooms_toggle,
            self.tab_visible_downloads_toggle,
            self.tab_visible_interests_toggle,
            self.tab_visible_private_chat_toggle,
            self.tab_visible_search_toggle,
            self.tab_visible_uploads_toggle,
            self.tab_visible_userinfo_toggle,
            self.tab_visible_userlist_toggle,
            self.tray_icon_toggle,
            self.tray_options_container
        ) = self.widgets = ui.load(scope=self, path="settings/userinterface.ui")

        self.application = application
        self.editing_color = False

        languages = [(_("System default"), "")]
        languages += [
            (language_name, language_code) for language_code, language_name in sorted(LANGUAGES)
        ]

        self.language_combobox = ComboBox(
            container=self.language_label.get_parent(), label=self.language_label,
            items=languages
        )

        self.close_action_combobox = ComboBox(
            container=self.close_action_label.get_parent(), label=self.close_action_label,
            items=(
                (_("Quit Nicotine+"), 0),
                (_("Show confirmation dialog"), 1),
                (_("Run in the background"), 2)
            )
        )

        self.chat_username_appearance_combobox = ComboBox(
            container=self.chat_username_appearance_label.get_parent(),
            label=self.chat_username_appearance_label,
            items=(
                (_("bold"), "bold"),
                (_("italic"), "italic"),
                (_("underline"), "underline"),
                (_("normal"), "normal")
            )
        )

        self.buddy_list_position_combobox = ComboBox(
            container=self.buddy_list_position_label.get_parent(), label=self.buddy_list_position_label,
            item_selected_callback=self.on_select_buddy_list_position,
            items=(
                (_("Separate Buddies tab"), "tab"),
                (_("Sidebar in Chat Rooms tab"), "chatrooms"),
                (_("Always visible sidebar"), "always")
            )
        )

        position_items = (
            (_("Top"), "Top"),
            (_("Bottom"), "Bottom"),
            (_("Left"), "Left"),
            (_("Right"), "Right")
        )

        self.tab_position_main_combobox = ComboBox(
            container=self.tab_position_main_label.get_parent(), label=self.tab_position_main_label,
            items=position_items)

        self.tab_position_search_combobox = ComboBox(
            container=self.tab_position_search_label.get_parent(), label=self.tab_position_search_label,
            items=position_items)

        self.tab_position_browse_combobox = ComboBox(
            container=self.tab_position_browse_label.get_parent(), label=self.tab_position_browse_label,
            items=position_items)

        self.tab_position_private_chat_combobox = ComboBox(
            container=self.tab_position_private_chat_label.get_parent(), label=self.tab_position_private_chat_label,
            items=position_items)

        self.tab_position_userinfo_combobox = ComboBox(
            container=self.tab_position_userinfo_label.get_parent(), label=self.tab_position_userinfo_label,
            items=position_items)

        self.tab_position_chatrooms_combobox = ComboBox(
            container=self.tab_position_chatrooms_label.get_parent(), label=self.tab_position_chatrooms_label,
            items=position_items)

        self.color_buttons = {
            "chatlocal": self.color_chat_local_button,
            "chatremote": self.color_chat_remote_button,
            "chatcommand": self.color_chat_command_button,
            "chatme": self.color_chat_action_button,
            "chathilite": self.color_chat_highlighted_button,
            "textbg": self.color_input_background_button,
            "inputcolor": self.color_input_text_button,
            "search": self.color_list_text_button,
            "useraway": self.color_status_away_button,
            "useronline": self.color_status_online_button,
            "useroffline": self.color_status_offline_button,
            "urlcolor": self.color_url_button,
            "tab_default": self.color_tab_button,
            "tab_hilite": self.color_tab_highlighted_button,
            "tab_changed": self.color_tab_changed_button
        }

        self.color_entries = {
            "chatlocal": self.color_chat_local_entry,
            "chatremote": self.color_chat_remote_entry,
            "chatcommand": self.color_chat_command_entry,
            "chatme": self.color_chat_action_entry,
            "chathilite": self.color_chat_highlighted_entry,
            "textbg": self.color_input_background_entry,
            "inputcolor": self.color_input_text_entry,
            "search": self.color_list_text_entry,
            "useraway": self.color_status_away_entry,
            "useronline": self.color_status_online_entry,
            "useroffline": self.color_status_offline_entry,
            "urlcolor": self.color_url_entry,
            "tab_default": self.color_tab_entry,
            "tab_hilite": self.color_tab_highlighted_entry,
            "tab_changed": self.color_tab_changed_entry
        }

        self.font_buttons = {
            "globalfont": self.font_global_button,
            "listfont": self.font_list_button,
            "textviewfont": self.font_text_view_button,
            "chatfont": self.font_chat_button,
            "searchfont": self.font_search_button,
            "transfersfont": self.font_transfers_button,
            "browserfont": self.font_browse_button
        }

        self.font_clear_buttons = {
            "globalfont": self.font_global_clear_button,
            "listfont": self.font_list_clear_button,
            "textviewfont": self.font_text_view_clear_button,
            "chatfont": self.font_chat_clear_button,
            "searchfont": self.font_search_clear_button,
            "transfersfont": self.font_transfers_clear_button,
            "browserfont": self.font_browse_clear_button
        }

        self.tab_position_comboboxes = {
            "tabmain": self.tab_position_main_combobox,
            "tabrooms": self.tab_position_chatrooms_combobox,
            "tabprivate": self.tab_position_private_chat_combobox,
            "tabsearch": self.tab_position_search_combobox,
            "tabinfo": self.tab_position_userinfo_combobox,
            "tabbrowse": self.tab_position_browse_combobox
        }

        self.tab_visible_toggles = {
            "search": self.tab_visible_search_toggle,
            "downloads": self.tab_visible_downloads_toggle,
            "uploads": self.tab_visible_uploads_toggle,
            "userbrowse": self.tab_visible_browse_toggle,
            "userinfo": self.tab_visible_userinfo_toggle,
            "private": self.tab_visible_private_chat_toggle,
            "userlist": self.tab_visible_userlist_toggle,
            "chatrooms": self.tab_visible_chatrooms_toggle,
            "interests": self.tab_visible_interests_toggle
        }

        rgba = Gdk.RGBA()
        rgba.red = rgba.green = rgba.blue = rgba.alpha = 0

        for color_id, button in self.color_buttons.items():
            button.set_rgba(rgba)
            button.connect("notify::rgba", self.on_color_button_changed, color_id)

        for color_id, entry in self.color_entries.items():
            entry.connect("icon-press", self.on_default_color, color_id)
            entry.connect("changed", self.on_color_entry_changed, color_id)

        for font_id, button in self.font_clear_buttons.items():
            button.connect("clicked", self.on_clear_font, font_id)

        if (GTK_API_VERSION, GTK_MINOR_VERSION) >= (4, 10):
            color_dialog = Gtk.ColorDialog()
            font_dialog = Gtk.FontDialog()

            for button in self.color_buttons.values():
                button.set_dialog(color_dialog)

            for button in self.font_buttons.values():
                button.set_dialog(font_dialog)
                button.set_level(Gtk.FontLevel.FONT)
        else:
            for button in self.color_buttons.values():
                button.set_use_alpha(True)

        icon_list = [
            (USER_STATUS_ICON_NAMES[UserStatus.ONLINE], _("Online"), 16, ("colored-icon", "user-status")),
            (USER_STATUS_ICON_NAMES[UserStatus.AWAY], _("Away"), 16, ("colored-icon", "user-status")),
            (USER_STATUS_ICON_NAMES[UserStatus.OFFLINE], _("Offline"), 16,
             ("colored-icon", "user-status")),
            ("nplus-tab-changed", _("Tab Changed"), 16, ("colored-icon", "notebook-tab-changed")),
            ("nplus-tab-highlight", _("Tab Highlight"), 16, ("colored-icon", "notebook-tab-highlight")),
            (pynicotine.__application_id__, _("Window"), 64, ())]

        if application.tray_icon.available:
            icon_list += [
                (f"{pynicotine.__application_id__}-connect", _("Online (Tray)"), 16, ()),
                (f"{pynicotine.__application_id__}-away", _("Away (Tray)"), 16, ()),
                (f"{pynicotine.__application_id__}-disconnect", _("Offline (Tray)"), 16, ()),
                (f"{pynicotine.__application_id__}-msg", _("Message (Tray)"), 16, ())]

        for icon_name, label, pixel_size, css_classes in icon_list:
            box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, valign=Gtk.Align.CENTER, spacing=6, visible=True)
            icon = Gtk.Image(icon_name=icon_name, pixel_size=pixel_size, visible=True)
            label = Gtk.Label(label=label, xalign=0.5, wrap=True, visible=True)

            for css_class in css_classes:
                add_css_class(icon, css_class)

            if GTK_API_VERSION >= 4:
                box.append(icon)   # pylint: disable=no-member
                box.append(label)  # pylint: disable=no-member
            else:
                box.add(icon)   # pylint: disable=no-member
                box.add(label)  # pylint: disable=no-member

            self.icon_view.insert(box, -1)

        self.icon_theme_button = FileChooserButton(
            self.icon_theme_label.get_parent(), window=application.preferences,
            label=self.icon_theme_label, end_button=self.icon_theme_clear_button, chooser_type="folder",
            show_open_external_button=not self.application.isolated_mode
        )

        self.options = {
            "notifications": {
                "notification_window_title": self.notification_window_title_toggle,
                "notification_popup_sound": self.notification_sounds_toggle,
                "notification_popup_file": self.notification_download_file_toggle,
                "notification_popup_folder": self.notification_download_folder_toggle,
                "notification_popup_queued_upload": self.notification_queued_upload_toggle,
                "notification_popup_private_message": self.notification_private_message_toggle,
                "notification_popup_chatroom": self.notification_chatroom_toggle,
                "notification_popup_chatroom_mention": self.notification_chatroom_mention_toggle,
                "notification_popup_wish": self.notification_wish_toggle
            },
            "ui": {
                "dark_mode": self.dark_mode_toggle,
                "exitdialog": self.close_action_combobox,
                "trayicon": self.tray_icon_toggle,
                "startup_hidden": self.minimize_tray_startup_toggle,
                "language": self.language_combobox,
                "reverse_file_paths": self.reverse_file_paths_toggle,
                "file_size_unit": self.exact_file_sizes_toggle,
                "tab_select_previous": self.tab_restore_startup_toggle,
                "tabclosers": self.tab_close_buttons_toggle,
                "icontheme": self.icon_theme_button,
                "chatlocal": self.color_chat_local_entry,
                "chatremote": self.color_chat_remote_entry,
                "chatcommand": self.color_chat_command_entry,
                "chatme": self.color_chat_action_entry,
                "chathilite": self.color_chat_highlighted_entry,
                "textbg": self.color_input_background_entry,
                "inputcolor": self.color_input_text_entry,
                "search": self.color_list_text_entry,
                "useraway": self.color_status_away_entry,
                "useronline": self.color_status_online_entry,
                "useroffline": self.color_status_offline_entry,
                "urlcolor": self.color_url_entry,
                "tab_default": self.color_tab_entry,
                "tab_hilite": self.color_tab_highlighted_entry,
                "tab_changed": self.color_tab_changed_entry,
                "usernamestyle": self.chat_username_appearance_combobox,
                "usernamehotspots": self.chat_colored_usernames_toggle,
                "buddylistinchatrooms": self.buddy_list_position_combobox,
                "header_bar": self.header_bar_toggle
            }
        }

        for dictionary in (
            self.font_buttons,
            self.tab_position_comboboxes
        ):
            self.options["ui"].update(dictionary)

    def destroy(self):

        self.language_combobox.destroy()
        self.close_action_combobox.destroy()
        self.chat_username_appearance_combobox.destroy()
        self.buddy_list_position_combobox.destroy()
        self.icon_theme_button.destroy()

        for combobox in self.tab_position_comboboxes.values():
            combobox.destroy()

        self.__dict__.clear()

    def set_settings(self):

        self.application.preferences.set_widgets_data(self.options)

        self.close_action_label.get_parent().set_visible(not self.application.isolated_mode)
        self.tray_options_container.set_visible(self.application.tray_icon.available)

        for page_id, enabled in config.sections["ui"]["modes_visible"].items():
            widget = self.tab_visible_toggles.get(page_id)

            if widget is not None:
                widget.set_active(enabled)

    def get_settings(self):

        enabled_tabs = {}

        for page_id, widget in self.tab_visible_toggles.items():
            enabled_tabs[page_id] = widget.get_active()

        return {
            "notifications": {
                "notification_window_title": self.notification_window_title_toggle.get_active(),
                "notification_popup_sound": self.notification_sounds_toggle.get_active(),
                "notification_popup_file": self.notification_download_file_toggle.get_active(),
                "notification_popup_folder": self.notification_download_folder_toggle.get_active(),
                "notification_popup_queued_upload": self.notification_queued_upload_toggle.get_active(),
                "notification_popup_private_message": self.notification_private_message_toggle.get_active(),
                "notification_popup_chatroom": self.notification_chatroom_toggle.get_active(),
                "notification_popup_chatroom_mention": self.notification_chatroom_mention_toggle.get_active(),
                "notification_popup_wish": self.notification_wish_toggle.get_active()
            },
            "ui": {
                "dark_mode": self.dark_mode_toggle.get_active(),
                "exitdialog": self.close_action_combobox.get_selected_id(),
                "trayicon": self.tray_icon_toggle.get_active(),
                "startup_hidden": self.minimize_tray_startup_toggle.get_active(),
                "language": self.language_combobox.get_selected_id(),
                "globalfont": self.get_font(self.font_global_button),
                "listfont": self.get_font(self.font_list_button),
                "textviewfont": self.get_font(self.font_text_view_button),
                "chatfont": self.get_font(self.font_chat_button),
                "searchfont": self.get_font(self.font_search_button),
                "transfersfont": self.get_font(self.font_transfers_button),
                "browserfont": self.get_font(self.font_browse_button),
                "reverse_file_paths": self.reverse_file_paths_toggle.get_active(),
                "file_size_unit": "B" if self.exact_file_sizes_toggle.get_active() else "",
                "tabmain": self.tab_position_main_combobox.get_selected_id(),
                "tabrooms": self.tab_position_chatrooms_combobox.get_selected_id(),
                "tabprivate": self.tab_position_private_chat_combobox.get_selected_id(),
                "tabsearch": self.tab_position_search_combobox.get_selected_id(),
                "tabinfo": self.tab_position_userinfo_combobox.get_selected_id(),
                "tabbrowse": self.tab_position_browse_combobox.get_selected_id(),
                "modes_visible": enabled_tabs,
                "tab_select_previous": self.tab_restore_startup_toggle.get_active(),
                "tabclosers": self.tab_close_buttons_toggle.get_active(),
                "icontheme": self.icon_theme_button.get_path(),
                "chatlocal": self.color_chat_local_entry.get_text().strip(),
                "chatremote": self.color_chat_remote_entry.get_text().strip(),
                "chatcommand": self.color_chat_command_entry.get_text().strip(),
                "chatme": self.color_chat_action_entry.get_text().strip(),
                "chathilite": self.color_chat_highlighted_entry.get_text().strip(),
                "urlcolor": self.color_url_entry.get_text().strip(),
                "textbg": self.color_input_background_entry.get_text().strip(),
                "inputcolor": self.color_input_text_entry.get_text().strip(),
                "search": self.color_list_text_entry.get_text().strip(),
                "useraway": self.color_status_away_entry.get_text().strip(),
                "useronline": self.color_status_online_entry.get_text().strip(),
                "useroffline": self.color_status_offline_entry.get_text().strip(),
                "tab_hilite": self.color_tab_highlighted_entry.get_text().strip(),
                "tab_default": self.color_tab_entry.get_text().strip(),
                "tab_changed": self.color_tab_changed_entry.get_text().strip(),
                "usernamestyle": self.chat_username_appearance_combobox.get_selected_id(),
                "usernamehotspots": self.chat_colored_usernames_toggle.get_active(),
                "buddylistinchatrooms": self.buddy_list_position_combobox.get_selected_id(),
                "header_bar": self.header_bar_toggle.get_active()
            }
        }

    # Icons #

    def on_clear_icon_theme(self, *_args):
        self.icon_theme_button.clear()

    # Fonts #

    def get_font(self, button):

        if GTK_API_VERSION >= 4:
            font_desc = button.get_font_desc()
            return font_desc.to_string() if font_desc.get_family() else ""

        return button.get_font()

    def on_clear_font(self, _button, font_id):

        font_button = self.font_buttons[font_id]

        if GTK_API_VERSION >= 4:
            font_button.set_font_desc(Pango.FontDescription())
        else:
            font_button.set_font("")

    # Colors #

    def on_color_entry_changed(self, entry, color_id):

        self.editing_color = True

        rgba = Gdk.RGBA()
        color_hex = entry.get_text().strip()

        if color_hex:
            rgba.parse(color_hex)
        else:
            rgba.red = rgba.green = rgba.blue = rgba.alpha = 0

        color_button = self.color_buttons[color_id]
        color_button.set_rgba(rgba)

        self.editing_color = False

    def on_color_button_changed(self, button, _param, color_id):

        if self.editing_color:
            return

        rgba = button.get_rgba()

        if rgba.alpha <= 0:
            # Unset color if transparent
            color_hex = ""
        else:
            red_color = round(rgba.red * 255)
            green_color = round(rgba.green * 255)
            blue_color = round(rgba.blue * 255)
            color_hex = f"#{red_color:02X}{green_color:02X}{blue_color:02X}"

            if rgba.alpha < 1 and GTK_API_VERSION >= 4:
                alpha_value = round(rgba.alpha * 255)
                color_hex += f"{alpha_value:02X}"

        entry = self.color_entries[color_id]

        if entry.get_text() != color_hex:
            entry.set_text(color_hex)

    def on_default_color(self, entry, *args):

        if GTK_API_VERSION >= 4:
            _icon_pos, color_id = args
        else:
            _icon_pos, _event, color_id = args

        entry.set_text(config.defaults["ui"][color_id])

    # Tabs #

    def on_select_buddy_list_position(self, _combobox, selected_id):
        self.tab_visible_userlist_toggle.set_active(selected_id == "tab")

    def on_buddy_list_tab_toggled(self, button):
        if button.get_active():
            self.buddy_list_position_combobox.set_selected_id("tab")


class LoggingPage:

    def __init__(self, application):

        (
            self.chatroom_log_folder_default_button,
            self.chatroom_log_folder_label,
            self.container,
            self.debug_log_folder_default_button,
            self.debug_log_folder_label,
            self.folder_locations_container,
            self.format_codes_label,
            self.log_chatroom_toggle,
            self.log_debug_toggle,
            self.log_private_chat_toggle,
            self.log_timestamp_format_entry,
            self.log_transfer_toggle,
            self.private_chat_log_folder_default_button,
            self.private_chat_log_folder_label,
            self.transfer_log_folder_default_button,
            self.transfer_log_folder_label
        ) = self.widgets = ui.load(scope=self, path="settings/log.ui")

        self.application = application

        format_codes_url = "https://docs.python.org/3/library/datetime.html#format-codes"
        format_codes_label = _("Format codes")

        self.format_codes_label.set_markup(
            f"<a href='{format_codes_url}' title='{format_codes_url}'>{format_codes_label}</a>")
        self.format_codes_label.connect("activate-link", self.on_activate_link)

        self.private_chat_log_folder_button = FileChooserButton(
            self.private_chat_log_folder_label.get_parent(), window=application.preferences,
            label=self.private_chat_log_folder_label, end_button=self.private_chat_log_folder_default_button,
            chooser_type="folder", show_open_external_button=not self.application.isolated_mode
        )
        self.chatroom_log_folder_button = FileChooserButton(
            self.chatroom_log_folder_label.get_parent(), window=application.preferences,
            label=self.chatroom_log_folder_label, end_button=self.chatroom_log_folder_default_button,
            chooser_type="folder", show_open_external_button=not self.application.isolated_mode
        )
        self.transfer_log_folder_button = FileChooserButton(
            self.transfer_log_folder_label.get_parent(), window=application.preferences,
            label=self.transfer_log_folder_label, end_button=self.transfer_log_folder_default_button,
            chooser_type="folder", show_open_external_button=not self.application.isolated_mode
        )
        self.debug_log_folder_button = FileChooserButton(
            self.debug_log_folder_label.get_parent(), window=application.preferences,
            label=self.debug_log_folder_label, end_button=self.debug_log_folder_default_button,
            chooser_type="folder", show_open_external_button=not self.application.isolated_mode
        )

        self.options = {
            "logging": {
                "privatechat": self.log_private_chat_toggle,
                "privatelogsdir": self.private_chat_log_folder_button,
                "chatrooms": self.log_chatroom_toggle,
                "roomlogsdir": self.chatroom_log_folder_button,
                "transfers": self.log_transfer_toggle,
                "transferslogsdir": self.transfer_log_folder_button,
                "debug_file_output": self.log_debug_toggle,
                "debuglogsdir": self.debug_log_folder_button,
                "log_timestamp": self.log_timestamp_format_entry
            }
        }

    def destroy(self):

        self.private_chat_log_folder_button.destroy()
        self.chatroom_log_folder_button.destroy()
        self.transfer_log_folder_button.destroy()
        self.debug_log_folder_button.destroy()

        self.__dict__.clear()

    def set_settings(self):

        self.application.preferences.set_widgets_data(self.options)

        self.format_codes_label.set_visible(not self.application.isolated_mode)
        self.folder_locations_container.set_margin_top(24 if self.application.isolated_mode else 0)

    def get_settings(self):

        return {
            "logging": {
                "privatechat": self.log_private_chat_toggle.get_active(),
                "privatelogsdir": self.private_chat_log_folder_button.get_path(),
                "chatrooms": self.log_chatroom_toggle.get_active(),
                "roomlogsdir": self.chatroom_log_folder_button.get_path(),
                "transfers": self.log_transfer_toggle.get_active(),
                "transferslogsdir": self.transfer_log_folder_button.get_path(),
                "debug_file_output": self.log_debug_toggle.get_active(),
                "debuglogsdir": self.debug_log_folder_button.get_path(),
                "log_timestamp": self.log_timestamp_format_entry.get_text()
            }
        }

    def on_activate_link(self, _label, url):
        open_uri(url)
        return True

    def on_default_timestamp(self, *_args):
        self.log_timestamp_format_entry.set_text(config.defaults["logging"]["log_timestamp"])

    def on_default_private_chat_log_folder(self, *_args):
        self.private_chat_log_folder_button.set_path(config.defaults["logging"]["privatelogsdir"])

    def on_default_chatroom_log_folder(self, *_args):
        self.chatroom_log_folder_button.set_path(config.defaults["logging"]["roomlogsdir"])

    def on_default_transfer_log_folder(self, *_args):
        self.transfer_log_folder_button.set_path(config.defaults["logging"]["transferslogsdir"])

    def on_default_debug_log_folder(self, *_args):
        self.debug_log_folder_button.set_path(config.defaults["logging"]["debuglogsdir"])


class SearchesPage:

    def __init__(self, application):

        (
            self.clear_filter_history_icon,
            self.clear_filter_history_success_icon,
            self.clear_search_history_icon,
            self.clear_search_history_success_icon,
            self.container,
            self.enable_default_filters_toggle,
            self.enable_search_history_toggle,
            self.filter_bitrate_entry,
            self.filter_country_entry,
            self.filter_exclude_entry,
            self.filter_file_size_entry,
            self.filter_file_type_entry,
            self.filter_free_slot_toggle,
            self.filter_help_button,
            self.filter_include_entry,
            self.filter_length_entry,
            self.filter_public_files_toggle,
            self.max_displayed_results_spinner,
            self.max_sent_results_spinner,
            self.min_search_term_length_spinner,
            self.repond_search_requests_toggle,
            self.show_private_results_toggle
        ) = self.widgets = ui.load(scope=self, path="settings/search.ui")

        self.application = application

        self.filter_help = SearchFilterHelp(application.preferences)
        self.filter_help.set_menu_button(self.filter_help_button)

        self.options = {
            "searches": {
                "maxresults": self.max_sent_results_spinner,
                "enablefilters": self.enable_default_filters_toggle,
                "defilter": None,
                "search_results": self.repond_search_requests_toggle,
                "max_displayed_results": self.max_displayed_results_spinner,
                "min_search_chars": self.min_search_term_length_spinner,
                "enable_history": self.enable_search_history_toggle,
                "private_search_results": self.show_private_results_toggle
            }
        }

    def destroy(self):
        self.filter_help.destroy()
        self.__dict__.clear()

    def set_settings(self):

        searches = config.sections["searches"]
        self.application.preferences.set_widgets_data(self.options)

        if searches["defilter"] is not None:
            num_filters = len(searches["defilter"])

            if num_filters > 0:
                self.filter_include_entry.set_text(str(searches["defilter"][0]))

            if num_filters > 1:
                self.filter_exclude_entry.set_text(str(searches["defilter"][1]))

            if num_filters > 2:
                self.filter_file_size_entry.set_text(str(searches["defilter"][2]))

            if num_filters > 3:
                self.filter_bitrate_entry.set_text(str(searches["defilter"][3]))

            if num_filters > 4:
                self.filter_free_slot_toggle.set_active(searches["defilter"][4])

            if num_filters > 5:
                self.filter_country_entry.set_text(str(searches["defilter"][5]))

            if num_filters > 6:
                self.filter_file_type_entry.set_text(str(searches["defilter"][6]))

            if num_filters > 7:
                self.filter_length_entry.set_text(str(searches["defilter"][7]))

            if num_filters > 8:
                self.filter_public_files_toggle.set_active(searches["defilter"][8])

        self.clear_search_history_icon.get_parent().set_visible_child(self.clear_search_history_icon)
        self.clear_filter_history_icon.get_parent().set_visible_child(self.clear_filter_history_icon)

    def get_settings(self):

        return {
            "searches": {
                "maxresults": self.max_sent_results_spinner.get_value_as_int(),
                "enablefilters": self.enable_default_filters_toggle.get_active(),
                "defilter": [
                    self.filter_include_entry.get_text().strip(),
                    self.filter_exclude_entry.get_text().strip(),
                    self.filter_file_size_entry.get_text().strip(),
                    self.filter_bitrate_entry.get_text().strip(),
                    self.filter_free_slot_toggle.get_active(),
                    self.filter_country_entry.get_text().strip(),
                    self.filter_file_type_entry.get_text().strip(),
                    self.filter_length_entry.get_text().strip(),
                    self.filter_public_files_toggle.get_active(),
                ],
                "search_results": self.repond_search_requests_toggle.get_active(),
                "max_displayed_results": self.max_displayed_results_spinner.get_value_as_int(),
                "min_search_chars": self.min_search_term_length_spinner.get_value_as_int(),
                "enable_history": self.enable_search_history_toggle.get_active(),
                "private_search_results": self.show_private_results_toggle.get_active()
            }
        }

    def on_clear_search_history(self, *_args):

        self.application.window.search.clear_search_history()

        stack = self.clear_search_history_success_icon.get_parent()
        stack.set_visible_child(self.clear_search_history_success_icon)

    def on_clear_filter_history(self, *_args):

        self.application.window.search.clear_filter_history()

        stack = self.clear_filter_history_success_icon.get_parent()
        stack.set_visible_child(self.clear_filter_history_success_icon)


class UrlHandlersPage:

    def __init__(self, application):

        (
            self.container,
            self.file_manager_label,
            self.protocol_list_container
        ) = self.widgets = ui.load(scope=self, path="settings/urlhandlers.ui")

        self.application = application

        self.file_manager_combobox = ComboBox(
            container=self.file_manager_label.get_parent(), label=self.file_manager_label, has_entry=True,
            items=(
                ("", None),
                ("xdg-open $", None),
                ("explorer $", None),
                ("nautilus $", None),
                ("nemo $", None),
                ("caja $", None),
                ("thunar $", None),
                ("dolphin $", None),
                ("konqueror $", None),
                ("krusader --left $", None),
                ("xterm -e mc $", None)
            )
        )

        self.options = {
            "urls": {
                "protocols": None
            },
            "ui": {
                "filemanager": self.file_manager_combobox
            }
        }

        self.default_protocols = [
            "http://",
            "https://",
            "audio",
            "image",
            "video",
            "document",
            "text",
            "archive",
            ".mp3",
            ".jpg",
            ".pdf"
        ]

        self.default_commands = [
            "xdg-open $",
            "firefox $",
            "firefox --new-tab $",
            "epiphany $",
            "chromium-browser $",
            "falkon $",
            "links -g $",
            "dillo $",
            "konqueror $",
            '"c:\\Program Files\\Mozilla Firefox\\Firefox.exe" $'
        ]

        self.protocols = {}
        self.protocol_list_view = TreeView(
            application.window, parent=self.protocol_list_container, multi_select=True,
            activate_row_callback=self.on_edit_handler,
            delete_accelerator_callback=self.on_remove_handler,
            columns={
                "protocol": {
                    "column_type": "text",
                    "title": _("Protocol"),
                    "width": 120,
                    "expand_column": True,
                    "iterator_key": True,
                    "default_sort_type": "ascending"
                },
                "command": {
                    "column_type": "text",
                    "title": _("Command"),
                    "expand_column": True
                }
            }
        )

        self.protocol_popup_menu = PopupMenu(application, self.protocol_list_view.widget)
        self.protocol_popup_menu.add_items(
            ("#" + _("_Edit…"), self.on_edit_handler),
            ("", None),
            ("#" + _("Remove"), self.on_remove_handler)
        )

    def destroy(self):

        self.protocol_popup_menu.destroy()
        self.file_manager_combobox.destroy()
        self.protocol_list_view.destroy()

        self.__dict__.clear()

    def set_settings(self):

        self.protocol_list_view.clear()
        self.protocol_list_view.freeze()
        self.protocols.clear()

        self.application.preferences.set_widgets_data(self.options)

        self.protocols = config.sections["urls"]["protocols"].copy()

        for protocol, command in self.protocols.items():
            self.protocol_list_view.add_row([str(protocol), str(command)], select_row=False)

        self.protocol_list_view.unfreeze()

    def get_settings(self):

        return {
            "urls": {
                "protocols": self.protocols.copy()
            },
            "ui": {
                "filemanager": self.file_manager_combobox.get_text().strip()
            }
        }

    def on_add_handler_response(self, dialog, _response_id, _data):

        protocol = dialog.get_entry_value().strip()
        command = dialog.get_second_entry_value().strip()

        if not protocol or not command:
            return

        if protocol.startswith("."):
            # Only keep last part of file extension (e.g. .tar.gz -> .gz)
            protocol = "." + protocol.rpartition(".")[-1]

        elif not protocol.endswith("://") and protocol not in self.default_protocols:
            protocol += "://"

        iterator = self.protocol_list_view.iterators.get(protocol)
        self.protocols[protocol] = command

        if iterator:
            self.protocol_list_view.set_row_value(iterator, "command", command)
            return

        self.protocol_list_view.add_row([protocol, command])

    def on_add_handler(self, *_args):

        EntryDialog(
            parent=self.application.preferences,
            title=_("Add URL Handler"),
            message=_("Enter the protocol and the command for the URL handler:"),
            action_button_label=_("_Add"),
            callback=self.on_add_handler_response,
            use_second_entry=True,
            droplist=self.default_protocols,
            second_droplist=self.default_commands
        ).present()

    def on_edit_handler_response(self, dialog, _response_id, iterator):

        command = dialog.get_entry_value().strip()

        if not command:
            return

        protocol = self.protocol_list_view.get_row_value(iterator, "protocol")

        self.protocols[protocol] = command
        self.protocol_list_view.set_row_value(iterator, "command", command)

    def on_edit_handler(self, *_args):

        for iterator in self.protocol_list_view.get_selected_rows():
            protocol = self.protocol_list_view.get_row_value(iterator, "protocol")
            command = self.protocol_list_view.get_row_value(iterator, "command")

            EntryDialog(
                parent=self.application.preferences,
                title=_("Edit Command"),
                message=_("Enter a new command for protocol %s:") % protocol,
                action_button_label=_("_Edit"),
                callback=self.on_edit_handler_response,
                callback_data=iterator,
                droplist=self.default_commands,
                default=command
            ).present()
            return

    def on_remove_handler(self, *_args):

        for iterator in reversed(list(self.protocol_list_view.get_selected_rows())):
            protocol = self.protocol_list_view.get_row_value(iterator, "protocol")
            orig_iterator = self.protocol_list_view.iterators[protocol]

            self.protocol_list_view.remove_row(orig_iterator)
            del self.protocols[protocol]

    def on_default_file_manager(self, *_args):
        default_file_manager = config.defaults["ui"]["filemanager"]
        self.file_manager_combobox.set_text(default_file_manager)


class NowPlayingPage:

    def __init__(self, application):

        (
            self.command_entry,
            self.command_label,
            self.container,
            self.format_help_label,
            self.format_message_label,
            self.lastfm_radio,
            self.librefm_radio,
            self.listenbrainz_radio,
            self.mpris_radio,
            self.other_radio,
            self.output_label,
            self.test_configuration_button
        ) = self.widgets = ui.load(scope=self, path="settings/nowplaying.ui")

        self.application = application
        self.enable_mpris = (
            not self.application.isolated_mode
            and sys.platform not in {"win32", "darwin"}
            and "SNAP_NAME" not in os.environ
        )
        self.enable_other = not self.application.isolated_mode

        self.format_message_combobox = ComboBox(
            container=self.format_message_label.get_parent(), label=self.format_message_label,
            has_entry=True
        )

        self.options = {
            "players": {
                "npformat": self.format_message_combobox,
                "npothercommand": self.command_entry
            }
        }

        self.player_replacers = []

        # Default format list
        self.default_format_list = [
            "$n",
            "$n ($f)",
            "/me np: $n",
            "$a - $t",
            "[$a] $t",
            "$a - $b - $t",
            "$a - $b - $t ($l/$r KBps) from $y $c"
        ]
        self.custom_format_list = []

        # Supply the information needed for the Now Playing class to return a song
        self.test_configuration_button.connect(
            "clicked",
            core.now_playing.display_now_playing,
            self.set_now_playing_output,   # Callback to update the song displayed
            self.get_player,               # Callback to retrieve selected player
            self.get_command,              # Callback to retrieve command text
            self.get_format                # Callback to retrieve format text
        )

        self.mpris_radio.set_visible(self.enable_mpris)
        self.other_radio.set_visible(self.enable_other)

    def destroy(self):
        self.format_message_combobox.destroy()
        self.__dict__.clear()

    def set_settings(self):

        # Add formats
        self.format_message_combobox.freeze()
        self.format_message_combobox.clear()

        for item in self.default_format_list:
            self.format_message_combobox.append(str(item))

        if self.custom_format_list:
            for item in self.custom_format_list:
                self.format_message_combobox.append(str(item))

        self.format_message_combobox.unfreeze()

        self.application.preferences.set_widgets_data(self.options)

        # Save reference to format list for get_settings()
        self.custom_format_list = config.sections["players"]["npformatlist"]

        # Update UI with saved player
        self.set_player(config.sections["players"]["npplayer"])
        self.update_now_playing_info()

    def get_player(self):

        player = "lastfm"

        if self.enable_mpris and self.mpris_radio.get_active():
            player = "mpris"

        elif self.enable_other and self.other_radio.get_active():
            player = "other"

        elif self.librefm_radio.get_active():
            player = "librefm"

        elif self.listenbrainz_radio.get_active():
            player = "listenbrainz"

        return player

    def get_command(self):
        return self.command_entry.get_text().strip()

    def get_format(self):
        return self.format_message_combobox.get_text()

    def set_player(self, player):

        if player == "mpris" and self.enable_mpris:
            self.mpris_radio.set_active(True)

        elif player == "other" and self.enable_other:
            self.other_radio.set_active(True)

        elif player == "librefm":
            self.librefm_radio.set_active(True)

        elif player == "listenbrainz":
            self.listenbrainz_radio.set_active(True)

        else:
            self.lastfm_radio.set_active(True)

    def update_now_playing_info(self, *_args):

        if self.lastfm_radio.get_active():
            self.player_replacers = ["$n", "$t", "$a", "$b"]
            self.command_label.set_text(_("Username;APIKEY"))

        if self.librefm_radio.get_active():
            self.player_replacers = ["$n", "$t", "$a", "$b"]
            self.command_label.set_text(_("Username: "))

        elif self.mpris_radio.get_active():
            self.player_replacers = ["$n", "$p", "$a", "$b", "$t", "$y", "$c", "$r", "$k", "$l", "$f"]
            self.command_label.set_text(_("Music player (e.g. amarok, audacious, exaile); leave empty to autodetect:"))

        elif self.listenbrainz_radio.get_active():
            self.player_replacers = ["$n", "$t", "$a", "$b"]
            self.command_label.set_text(_("Username: "))

        elif self.other_radio.get_active():
            self.player_replacers = ["$n"]
            self.command_label.set_text(_("Command:"))

        legend = ""

        for item in self.player_replacers:
            legend += item + "\t"

            if item == "$t":
                legend += _("Title")
            elif item == "$n":
                legend += _('Now Playing (typically "%(artist)s - %(title)s")') % {
                    "artist": _("Artist"), "title": _("Title")}
            elif item == "$l":
                legend += _("Duration")
            elif item == "$r":
                legend += _("Bitrate")
            elif item == "$c":
                legend += _("Comment")
            elif item == "$a":
                legend += _("Artist")
            elif item == "$b":
                legend += _("Album")
            elif item == "$k":
                legend += _("Track Number")
            elif item == "$y":
                legend += _("Year")
            elif item == "$f":
                legend += _("Filename (URI)")
            elif item == "$p":
                legend += _("Program")

            legend += "\n"

        self.format_help_label.set_text(legend[:-1])

    def set_now_playing_output(self, title):
        self.output_label.set_text(title)

    def get_settings(self):

        npformat = self.get_format()

        if (npformat and not npformat.isspace()
                and npformat not in self.custom_format_list
                and npformat not in self.default_format_list):
            self.custom_format_list.append(npformat)

        return {
            "players": {
                "npplayer": self.get_player(),
                "npothercommand": self.get_command(),
                "npformat": npformat,
                "npformatlist": self.custom_format_list
            }
        }


class PluginsPage:

    def __init__(self, application):

        (
            self.container,
            self.enable_plugins_toggle,
            self.info_bar_container,
            self.plugin_authors_label,
            self.plugin_description_view_container,
            self.plugin_list_container,
            self.plugin_name_label,
            self.plugin_settings_button,
            self.plugin_version_label
        ) = self.widgets = ui.load(scope=self, path="settings/plugin.ui")

        self.application = application
        self.selected_plugin = None
        self.plugin_settings_dialog = None

        self.options = {
            "plugins": {
                "enable": self.enable_plugins_toggle
            }
        }

        self.info_bar = InfoBar(parent=self.info_bar_container)
        self.plugin_description_view = TextView(self.plugin_description_view_container, editable=False,
                                                pixels_below_lines=2)
        self.plugin_list_view = TreeView(
            application.window, parent=self.plugin_list_container,
            activate_row_callback=self.on_row_activated,
            delete_accelerator_callback=self.on_uninstall_plugin,
            select_row_callback=self.on_select_plugin,
            columns={
                # Visible columns
                "enabled": {
                    "column_type": "toggle",
                    "title": _("Enabled"),
                    "width": 0,
                    "toggle_callback": self.on_toggle_plugin,
                    "hide_header": True
                },
                "human_name": {
                    "column_type": "text",
                    "title": _("Plugin"),
                    "default_sort_type": "ascending"
                },

                # Hidden data columns
                "name_data": {"data_type": GObject.TYPE_STRING, "iterator_key": True}
            }
        )

        self.plugin_popup_menu = PopupMenu(application, self.plugin_list_view.widget, self.on_plugin_popup_menu)
        self.plugin_popup_menu.add_items(
            ("=" + _("_Enable"), self.on_toggle_selected_plugin),
            ("=" + _("_Disable"), self.on_toggle_selected_plugin),
            ("#" + _("_Settings"), self.on_show_plugin_settings),
            ("", None),
            ("=" + _("Open in File _Manager"), self.on_open_file_manager),
            ("=" + _("_Uninstall…"), self.on_uninstall_plugin)
        )

    def destroy(self):

        self.plugin_popup_menu.destroy()
        self.info_bar.destroy()
        self.plugin_description_view.destroy()
        self.plugin_list_view.destroy()

        if self.plugin_settings_dialog is not None:
            self.plugin_settings_dialog.destroy()

        self.__dict__.clear()

    def set_settings(self):

        self.plugin_list_view.clear()
        self.plugin_list_view.freeze()

        self.application.preferences.set_widgets_data(self.options)

        for plugin_name in core.pluginhandler.list_installed_plugins():
            try:
                info = core.pluginhandler.get_plugin_info(plugin_name)
            except OSError:
                continue

            plugin_human_name = info.get("Name", plugin_name)
            enabled = (plugin_name in config.sections["plugins"]["enabled"])
            self.plugin_list_view.add_row([enabled, plugin_human_name, plugin_name], select_row=False)

        self.plugin_list_view.unfreeze()

    def get_settings(self):

        return {
            "plugins": {
                "enable": self.enable_plugins_toggle.get_active()
            }
        }

    def check_plugin_settings_button(self, plugin_name):
        self.plugin_settings_button.set_sensitive(bool(core.pluginhandler.get_plugin_metasettings(plugin_name)))

    def on_plugin_popup_menu(self, menu, _widget):

        is_internal_plugin = core.pluginhandler.is_internal_plugin(self.selected_plugin)

        for iterator in self.plugin_list_view.get_selected_rows():
            enabled = self.plugin_list_view.get_row_value(iterator, "enabled")

            menu.actions[_("_Enable")].set_enabled(not enabled)
            menu.actions[_("_Disable")].set_enabled(enabled)
            break

        menu.actions[_("_Settings")].set_enabled(self.plugin_settings_button.get_sensitive())
        menu.actions[_("Open in File _Manager")].set_enabled(
            not self.application.isolated_mode and not is_internal_plugin
        )
        menu.actions[_("_Uninstall…")].set_enabled(not is_internal_plugin)

    def on_select_plugin(self, list_view, iterator):

        if iterator is None:
            self.selected_plugin = _("No Plugin Selected")
            info = {}
        else:
            self.selected_plugin = list_view.get_row_value(iterator, "name_data")
            info = core.pluginhandler.get_plugin_info(self.selected_plugin)

        plugin_human_name = info.get("Name", self.selected_plugin)
        plugin_version = info.get("Version", "-")
        plugin_authors = ", ".join(info.get("Authors", "-"))
        plugin_description = info.get("Description", "").replace(r"\n", "\n")

        self.plugin_name_label.set_text(plugin_human_name)
        self.plugin_version_label.set_text(plugin_version)
        self.plugin_authors_label.set_text(plugin_authors)

        self.plugin_description_view.clear()
        self.plugin_description_view.add_line(plugin_description)
        self.plugin_description_view.place_cursor_at_line(0)

        if iterator is not None and not core.pluginhandler.is_internal_plugin(self.selected_plugin):
            self.info_bar.show_warning_message(
                _("This is not a built-in plugin. Use at your own risk.")
            )
        else:
            self.info_bar.set_visible(False)

        self.check_plugin_settings_button(self.selected_plugin)

    def on_toggle_plugin(self, list_view, iterator):

        plugin_name = list_view.get_row_value(iterator, "name_data")
        enabled = core.pluginhandler.toggle_plugin(plugin_name)

        list_view.set_row_value(iterator, "enabled", enabled)
        self.check_plugin_settings_button(plugin_name)

    def on_toggle_selected_plugin(self, *_args):

        for iterator in self.plugin_list_view.get_selected_rows():
            self.on_toggle_plugin(self.plugin_list_view, iterator)
            return

    def on_toggle_enable_plugins(self, *_args):

        plugins_enabled = config.sections["plugins"]["enabled"].copy()

        if self.enable_plugins_toggle.get_active():
            # Enable all selected plugins
            for plugin_name in plugins_enabled:
                core.pluginhandler.enable_plugin(plugin_name)

            self.check_plugin_settings_button(self.selected_plugin)
            return

        # Disable all plugins
        for plugin_name in core.pluginhandler.enabled_plugins.copy():
            core.pluginhandler.disable_plugin(plugin_name)

        config.sections["plugins"]["enabled"] = plugins_enabled
        self.plugin_settings_button.set_sensitive(False)

    def on_install_plugin_selected(self, selected_file_paths, _data):

        plugin_name = None

        for file_path in selected_file_paths:
            plugin_name = core.pluginhandler.install_plugin(file_path)
            break

        self.set_settings()

        iterator = self.plugin_list_view.iterators.get(plugin_name)

        if iterator is not None:
            self.plugin_list_view.select_row(iterator)

    def on_install_plugin(self, *_args):

        from pynicotine.gtkgui.widgets.filechooser import FileChooser

        FileChooser(
            parent=self.application.preferences,
            title=_("Select a Zip File"),
            callback=self.on_install_plugin_selected
        ).present()

    def on_uninstall_plugin_response(self, _dialog, _response_id, selected_plugin):
        core.pluginhandler.uninstall_plugin(selected_plugin)
        self.set_settings()

    def on_uninstall_plugin(self, *_args):

        if core.pluginhandler.is_internal_plugin(self.selected_plugin):
            return

        try:
            info = core.pluginhandler.get_plugin_info(self.selected_plugin)
            human_plugin_name = info.get("Name", self.selected_plugin)
        except OSError:
            human_plugin_name = self.selected_plugin

        OptionDialog(
            parent=self,
            title=_("Uninstall Plugin?"),
            message=_("Do you really want to uninstall plugin %s? "
                      "This will remove any files the plugin has stored.") % human_plugin_name,
            destructive_response_id="ok",
            callback=self.on_uninstall_plugin_response,
            callback_data=self.selected_plugin
        ).present()

    def on_open_file_manager(self, *_args):

        if self.selected_plugin is None:
            return

        folder_path = core.pluginhandler.get_plugin_path(self.selected_plugin)
        open_folder_path(folder_path, create_folder=True)

    def on_show_plugin_settings(self, *_args):

        if self.selected_plugin is None:
            return

        metasettings = core.pluginhandler.get_plugin_metasettings(self.selected_plugin)

        if not metasettings:
            return

        if self.plugin_settings_dialog is None:
            self.plugin_settings_dialog = PluginSettings(self.application)

        self.plugin_settings_dialog.load_options(self.selected_plugin, metasettings)
        self.plugin_settings_dialog.present()

    def on_row_activated(self, _list_view, _iterator, column_id):
        if column_id == "human_name":
            self.on_show_plugin_settings()


class Preferences(Dialog):

    def __init__(self, application):

        self.application = application

        (
            self.apply_button,
            self.cancel_button,
            self.container,
            self.content,
            self.export_button,
            self.ok_button,
            self.preferences_list,
            self.viewport
        ) = self.widgets = ui.load(scope=self, path="dialogs/preferences.ui")

        super().__init__(
            parent=application.window,
            modal=False,
            content_box=self.container,
            buttons_start=(self.cancel_button, self.export_button),
            buttons_end=(self.apply_button, self.ok_button),
            default_button=self.ok_button,
            close_callback=self.on_close,
            title=_("Preferences"),
            width=960,
            height=650,
            show_title_buttons=False
        )
        add_css_class(self.widget, "preferences-border")
        application.add_window(self.widget)

        if GTK_API_VERSION == 3:
            # Scroll to focused widgets
            self.viewport.set_focus_vadjustment(self.content.get_vadjustment())

        self.pages = {}
        self.page_ids = [
            ("network", NetworkPage, _("Network"), "network-wireless-symbolic"),
            ("user-interface", UserInterfacePage, _("User Interface"), "view-grid-symbolic"),
            ("shares", SharesPage, _("Shares"), "folder-symbolic"),
            ("downloads", DownloadsPage, _("Downloads"), "folder-download-symbolic"),
            ("uploads", UploadsPage, _("Uploads"), "emblem-shared-symbolic"),
            ("searches", SearchesPage, _("Searches"), "system-search-symbolic"),
            ("user-profile", UserProfilePage, _("User Profile"), "avatar-default-symbolic"),
            ("chats", ChatsPage, _("Chats"), "insert-text-symbolic"),
            ("now-playing", NowPlayingPage, _("Now Playing"), "folder-music-symbolic"),
            ("logging", LoggingPage, _("Logging"), "folder-documents-symbolic"),
            ("banned-users", BannedUsersPage, _("Banned Users"), "action-unavailable-symbolic"),
            ("ignored-users", IgnoredUsersPage, _("Ignored Users"), "microphone-sensitivity-muted-symbolic"),
            ("url-handlers", UrlHandlersPage, _("URL Handlers"), "insert-link-symbolic"),
            ("plugins", PluginsPage, _("Plugins"), "application-x-addon-symbolic")
        ]

        for item in self.page_ids[:]:
            page_id, _page_class, label, icon_name = item

            if self.application.isolated_mode and page_id == "url-handlers":
                self.page_ids.remove(item)
                continue

            box = Gtk.Box(margin_top=8, margin_bottom=8, margin_start=12, margin_end=12, spacing=12, visible=True)
            icon = Gtk.Image(icon_name=icon_name, visible=True)
            label = Gtk.Label(label=label, xalign=0, visible=True)

            if GTK_API_VERSION >= 4:
                box.append(icon)   # pylint: disable=no-member
                box.append(label)  # pylint: disable=no-member
            else:
                box.add(icon)   # pylint: disable=no-member
                box.add(label)  # pylint: disable=no-member

            self.preferences_list.insert(box, -1)

        Accelerator("Tab", self.preferences_list, self.on_sidebar_tab_accelerator)
        Accelerator("<Shift>Tab", self.preferences_list, self.on_sidebar_shift_tab_accelerator)

    def destroy(self):

        for page in self.pages.values():
            page.destroy()

        super().destroy()

    def set_active_page(self, page_id):

        if page_id is None:
            return

        for index, (n_page_id, _page_class, _label, _icon_name) in enumerate(self.page_ids):
            if n_page_id != page_id:
                continue

            row = self.preferences_list.get_row_at_index(index)
            self.preferences_list.select_row(row)
            break

    def set_widgets_data(self, options):

        for section, keys in options.items():
            if section not in config.sections:
                continue

            for key in keys:
                widget = options[section][key]

                if widget is None:
                    continue

                self.set_widget(widget, config.sections[section][key])

    @staticmethod
    def set_widget(widget, value):

        if isinstance(widget, Gtk.SpinButton):
            try:
                widget.set_value(value)

            except TypeError:
                # Not a numerical value
                pass

        elif isinstance(widget, Gtk.Entry):
            if isinstance(value, (str, int)) and widget.get_text() != value:
                widget.set_text(value)

        elif isinstance(widget, TextView):
            if isinstance(value, str):
                widget.set_text(unescape(value))

        elif isinstance(widget, Gtk.Switch):
            widget.set_active(value)

        elif isinstance(widget, Gtk.CheckButton):
            try:
                # Radio button
                if isinstance(value, int) and value < len(widget.group_radios):
                    widget.group_radios[value].set_active(True)

            except (AttributeError, TypeError):
                # Regular check button
                widget.set_active(value)

        elif isinstance(widget, ComboBox):
            if widget.entry is not None:
                widget.set_text(value)
            else:
                widget.set_selected_id(value)

        elif isinstance(widget, Gtk.FontButton):
            widget.set_font(value)

        elif isinstance(widget, TreeView):
            widget.freeze()

            if isinstance(value, list):
                for item in value:
                    if isinstance(item, list):
                        row = item
                    else:
                        row = [item]

                    widget.add_row(row, select_row=False)

            elif isinstance(value, dict):
                for item1, item2 in value.items():
                    widget.add_row([str(item1), str(item2)], select_row=False)

            widget.unfreeze()

        elif isinstance(widget, FileChooserButton):
            widget.set_path(value)

        elif isinstance(widget, Gtk.FontDialogButton):
            widget.set_font_desc(Pango.FontDescription.from_string(value))

    def set_settings(self):

        for page in self.pages.values():
            page.set_settings()

    def get_settings(self):

        options = {
            "server": {},
            "transfers": {},
            "userinfo": {},
            "logging": {},
            "searches": {},
            "privatechat": {},
            "ui": {},
            "urls": {},
            "players": {},
            "words": {},
            "notifications": {},
            "plugins": {}
        }

        for page in self.pages.values():
            for key, data in page.get_settings().items():
                options[key].update(data)

        for section, key in (
            ("server", "login"),
            ("server", "portrange"),
            ("server", "interface"),
            ("server", "server")
        ):
            reconnect_required = self.has_option_changed(options, section, key)

            if reconnect_required:
                break

        portmap_changed = self.has_option_changed(options, "server", "upnp")
        portmap_required = None

        if portmap_changed:
            portmap_required = "add" if options["server"]["upnp"] else "remove"

        for section, key in (
            ("transfers", "shared"),
            ("transfers", "buddyshared"),
            ("transfers", "trustedshared"),
            ("transfers", "share_filters")
        ):
            rescan_required = self.has_option_changed(options, section, key)

            if rescan_required:
                break

        for section, key in (
            ("transfers", "rescan_shares_daily"),
            ("transfers", "rescan_shares_hour")
        ):
            rescan_daily_required = self.has_option_changed(options, section, key)

            if rescan_daily_required:
                break

        for section, key in (
            ("transfers", "reveal_buddy_shares"),
            ("transfers", "reveal_trusted_shares")
        ):
            recompress_shares_required = self.has_option_changed(options, section, key)

            if recompress_shares_required:
                break

        for section, key in (
            ("userinfo", "descr"),
            ("userinfo", "pic")
        ):
            user_profile_required = self.has_option_changed(options, section, key)

            if user_profile_required:
                break

        for section, key in (
            ("words", "tab"),
            ("words", "dropdown"),
            ("words", "characters"),
            ("words", "roomnames"),
            ("words", "buddies"),
            ("words", "roomusers"),
            ("words", "commands")
        ):
            completion_required = self.has_option_changed(options, section, key)

            if completion_required:
                break

        private_room_required = self.has_option_changed(options, "server", "private_chatrooms")
        search_history_required = self.has_option_changed(options, "searches", "enable_history")

        return (
            reconnect_required,
            portmap_required,
            rescan_required,
            rescan_daily_required,
            recompress_shares_required,
            user_profile_required,
            private_room_required,
            completion_required,
            search_history_required,
            options
        )

    def has_option_changed(self, options, section, key):

        if key not in options[section]:
            return False

        return options[section][key] != config.sections[section][key]

    def update_settings(self, settings_closed=False):

        (
            reconnect_required,
            portmap_required,
            rescan_required,
            rescan_daily_required,
            recompress_shares_required,
            user_profile_required,
            private_room_required,
            completion_required,
            search_history_required,
            options
        ) = self.get_settings()

        for key, data in options.items():
            config.sections[key].update(data)

        banned_page = self.pages.get("banned-users")
        ignored_page = self.pages.get("ignored-users")

        if banned_page is not None:
            for username in banned_page.added_users:
                core.network_filter.ban_user(username)

            for username, ip_address in banned_page.added_ips:
                core.network_filter.ban_user_ip(username, ip_address)

            for username in banned_page.removed_users:
                core.network_filter.unban_user(username)

            for username, ip_address in banned_page.removed_ips:
                core.network_filter.unban_user_ip(username, ip_address)

            banned_page.clear_changes()

        if ignored_page is not None:
            for username in ignored_page.added_users:
                core.network_filter.ignore_user(username)

            for username, ip_address in ignored_page.added_ips:
                core.network_filter.ignore_user_ip(username, ip_address)

            for username in ignored_page.removed_users:
                core.network_filter.unignore_user(username)

            for username, ip_address in ignored_page.removed_ips:
                core.network_filter.unignore_user_ip(username, ip_address)

            ignored_page.clear_changes()

        if reconnect_required:
            core.reconnect()

        if portmap_required == "add":
            core.portmapper.add_port_mapping()

        elif portmap_required == "remove":
            core.portmapper.remove_port_mapping()

        if user_profile_required:
            core.userinfo.show_user(refresh=True, switch_page=False)

        if private_room_required:
            active = config.sections["server"]["private_chatrooms"]
            self.application.window.chatrooms.room_list.toggle_accept_private_room(active)

        if completion_required:
            core.chatrooms.update_completions()
            core.privatechat.update_completions()

        if search_history_required:
            self.application.window.search.populate_search_history()

        if not rescan_required:
            if rescan_daily_required:
                core.shares.start_rescan_daily_timer()

            if recompress_shares_required:
                core.shares.rescan_shares(init=True, rescan=False)

        # Dark mode
        dark_mode_state = config.sections["ui"]["dark_mode"]
        set_dark_mode(dark_mode_state)

        # Header bar
        header_bar_state = config.sections["ui"]["header_bar"]
        self.application.window.set_use_header_bar(header_bar_state)

        # Icons
        load_custom_icons(update=True)

        # Fonts and colors
        update_custom_css()

        # Chats
        self.application.window.chatrooms.update_widgets()
        self.application.window.privatechat.update_widgets()

        # Buddies
        self.application.window.buddies.set_buddy_list_position()

        # Transfers
        core.downloads.update_transfer_limits()
        core.downloads.update_download_filters()
        core.uploads.update_transfer_limits()

        # Logging
        log.update_folder_paths()

        # Tray icon
        if not config.sections["ui"]["trayicon"]:
            self.application.tray_icon.unload(is_shutdown=False)
        else:
            self.application.tray_icon.load()

        # Main notebook
        self.application.window.set_tab_positions()
        self.application.window.set_main_tabs_visibility()

        for tab in self.application.window.tabs.values():
            self.application.window.set_tab_expand(tab.page)

        # Other notebooks
        for notebook in (self.application.window.chatrooms, self.application.window.privatechat,
                         self.application.window.userinfo, self.application.window.userbrowse,
                         self.application.window.search):
            notebook.set_tab_closers()

        # Update configuration
        config.write_configuration()

        if not settings_closed:
            return

        self.close()

        if not config.sections["ui"]["trayicon"]:
            self.application.window.present()

        if rescan_required:
            core.shares.rescan_shares()

        if config.need_config():
            core.setup()

    @staticmethod
    def on_back_up_config_response(selected, _data):

        file_path = next(iter(selected), None)

        if file_path:
            config.write_config_backup(file_path)

    def on_back_up_config(self, *_args):

        current_date_time = time.strftime("%Y-%m-%d_%H-%M-%S")

        FileChooserSave(
            parent=self,
            callback=self.on_back_up_config_response,
            initial_folder=os.path.dirname(config.config_file_path),
            initial_file=f"config_backup_{current_date_time}.tar.bz2",
            title=_("Pick a File Name for Config Backup")
        ).present()

    def on_toggle_label_pressed(self, _controller, _num_p, _pos_x, _pos_y, toggle):
        toggle.emit("activate")

    def on_widget_scroll_event(self, _widget, event):
        """Prevent scrolling in GtkSpinButton and pass scroll event to container (GTK 3)"""

        self.content.event(event)
        return True

    def on_widget_scroll(self, _controller, _scroll_x, scroll_y):
        """Prevent scrolling in GtkSpinButton and emulate scrolling in the container (GTK 4)"""

        adjustment = self.content.get_vadjustment()
        value = adjustment.get_value()

        if scroll_y < 0:
            value -= adjustment.get_step_increment()
        else:
            value += adjustment.get_step_increment()

        adjustment.set_value(value)
        return True

    def on_switch_page(self, _listbox, row):

        if row is None:
            return

        page_id, page_class, _label, _icon_name = self.page_ids[row.get_index()]
        old_page = self.viewport.get_child()

        if old_page:
            if GTK_API_VERSION >= 4:
                self.viewport.set_child(None)
            else:
                self.viewport.remove(old_page)

        if page_id not in self.pages:
            self.pages[page_id] = page = page_class(self.application)
            page.set_settings()

            for obj in page.widgets:
                if isinstance(obj, Gtk.CheckButton):
                    if GTK_API_VERSION >= 4:
                        try:
                            check_button_label = list(obj)[-1]
                            check_button_label.set_wrap(True)   # pylint: disable=no-member
                        except AttributeError:
                            pass
                    else:
                        check_button_label = obj.get_child()
                        check_button_label.set_line_wrap(True)  # pylint: disable=no-member
                        obj.set_receives_default(True)

                elif isinstance(obj, Gtk.Switch):
                    switch_container = obj.get_parent()
                    switch_label = next(iter(switch_container))

                    if GTK_API_VERSION >= 4:
                        switch_label.gesture_click = Gtk.GestureClick()
                        switch_label.add_controller(switch_label.gesture_click)
                    else:
                        switch_label.set_has_window(True)
                        switch_label.gesture_click = Gtk.GestureMultiPress(widget=switch_label)

                    obj.set_receives_default(True)
                    switch_label.gesture_click.connect("released", self.on_toggle_label_pressed, obj)

                elif isinstance(obj, Gtk.SpinButton):
                    if GTK_API_VERSION >= 4:
                        scroll_controller = Gtk.EventControllerScroll(
                            flags=int(Gtk.EventControllerScrollFlags.VERTICAL)
                        )
                        scroll_controller.connect("scroll", self.on_widget_scroll)
                        obj.add_controller(scroll_controller)
                    else:
                        obj.connect("scroll-event", self.on_widget_scroll_event)

                elif (isinstance(obj, Gtk.FontButton)
                      or ((GTK_API_VERSION, GTK_MINOR_VERSION) >= (4, 10) and isinstance(obj, Gtk.FontDialogButton))):
                    if GTK_API_VERSION >= 4:
                        inner_button = next(iter(obj))
                        font_button_container = next(iter(inner_button))
                        font_button_label = next(iter(font_button_container))
                    else:
                        font_button_container = obj.get_child()
                        font_button_label = next(iter(font_button_container))

                    try:
                        font_button_label.set_ellipsize(Pango.EllipsizeMode.END)
                    except AttributeError:
                        pass

            page.container.set_margin_start(18)
            page.container.set_margin_end(18)
            page.container.set_margin_top(14)
            page.container.set_margin_bottom(18)

        if GTK_API_VERSION >= 4:
            self.viewport.set_child(self.pages[page_id].container)  # pylint: disable=no-member
        else:
            self.viewport.add(self.pages[page_id].container)        # pylint: disable=no-member

        # Scroll to the top
        self.content.get_vadjustment().set_value(0)

    def on_sidebar_tab_accelerator(self, *_args):
        """Tab - navigate to widget after preferences sidebar."""

        self.content.child_focus(Gtk.DirectionType.TAB_FORWARD)
        return True

    def on_sidebar_shift_tab_accelerator(self, *_args):
        """Shift+Tab - navigate to widget before preferences sidebar."""

        self.ok_button.grab_focus()
        return True

    def on_cancel(self, *_args):
        self.close()

    def on_apply(self, *_args):
        self.update_settings()

    def on_ok(self, *_args):
        self.update_settings(settings_closed=True)

    def on_close(self, *_args):
        self.content.get_vadjustment().set_value(0)
